n8n-nodes-docx-filler 2.1.0 → 2.3.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.
|
@@ -197,6 +197,32 @@ function validateFieldValue(fieldType, value) {
|
|
|
197
197
|
const cleanValue = value.trim();
|
|
198
198
|
return field.valuePatterns.some(pattern => pattern.test(cleanValue));
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Détecte le type de document à partir des paragraphes
|
|
202
|
+
*/
|
|
203
|
+
function detectDocumentType(paragraphs) {
|
|
204
|
+
for (const p of paragraphs) {
|
|
205
|
+
const text = p.textNormalized;
|
|
206
|
+
// AE - Acte d'Engagement (vérifier en premier car plus spécifique)
|
|
207
|
+
if (text.includes('acte d\'engagement') || text.includes('acte d engagement') ||
|
|
208
|
+
text.includes('engagement du titulaire')) {
|
|
209
|
+
return 'AE';
|
|
210
|
+
}
|
|
211
|
+
// DC1 - Lettre de candidature
|
|
212
|
+
if (text.includes('dc1') || text.includes('lettre de candidature')) {
|
|
213
|
+
return 'DC1';
|
|
214
|
+
}
|
|
215
|
+
// DC2 - Déclaration du candidat
|
|
216
|
+
if (text.includes('dc2') || text.includes('déclaration du candidat individuel')) {
|
|
217
|
+
return 'DC2';
|
|
218
|
+
}
|
|
219
|
+
// ATTRI1 - Attribution
|
|
220
|
+
if (text.includes('attri1')) {
|
|
221
|
+
return 'ATTRI1';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return 'unknown';
|
|
225
|
+
}
|
|
200
226
|
/**
|
|
201
227
|
* Extrait les données d'un document source
|
|
202
228
|
*/
|
|
@@ -209,12 +235,21 @@ function extractSourceData(paragraphs) {
|
|
|
209
235
|
for (let i = 0; i < paragraphs.length; i++) {
|
|
210
236
|
const p = paragraphs[i];
|
|
211
237
|
const text = p.text.trim();
|
|
212
|
-
// Détecter le type de document
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
238
|
+
// Détecter le type de document (priorité au premier match explicite)
|
|
239
|
+
if (documentType === 'unknown') {
|
|
240
|
+
if (p.textNormalized.includes('acte d\'engagement') || p.textNormalized.includes('acte d engagement') ||
|
|
241
|
+
(p.textNormalized.includes('ae') && p.textNormalized.includes('formulaire'))) {
|
|
242
|
+
documentType = 'AE';
|
|
243
|
+
}
|
|
244
|
+
else if (p.textNormalized.includes('dc1') || p.textNormalized.includes('lettre de candidature')) {
|
|
245
|
+
documentType = 'DC1';
|
|
246
|
+
}
|
|
247
|
+
else if (p.textNormalized.includes('dc2') || p.textNormalized.includes('déclaration du candidat')) {
|
|
248
|
+
documentType = 'DC2';
|
|
249
|
+
}
|
|
250
|
+
else if (p.textNormalized.includes('attri1') || p.textNormalized.includes('attribution')) {
|
|
251
|
+
documentType = 'ATTRI1';
|
|
252
|
+
}
|
|
218
253
|
}
|
|
219
254
|
// Extraire les checkboxes
|
|
220
255
|
if (p.hasCheckbox) {
|
|
@@ -530,6 +565,122 @@ async function getDocumentBuffer(context, itemIndex, inputType, inputValue, item
|
|
|
530
565
|
validateDocxBuffer(buffer, source);
|
|
531
566
|
return buffer;
|
|
532
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Génère un rapport de vérification basique (sans LLM)
|
|
570
|
+
*/
|
|
571
|
+
function generateBasicVerificationReport(sourceData, templateDocType, filledFields, fillPositions) {
|
|
572
|
+
const fieldDetails = [];
|
|
573
|
+
const warnings = [];
|
|
574
|
+
const suggestions = [];
|
|
575
|
+
// Analyser les champs remplis
|
|
576
|
+
for (const field of sourceData.fields) {
|
|
577
|
+
const wasFilled = filledFields.some(f => f.includes(field.fieldType));
|
|
578
|
+
fieldDetails.push({
|
|
579
|
+
fieldType: field.fieldType,
|
|
580
|
+
label: field.label,
|
|
581
|
+
filledValue: field.value,
|
|
582
|
+
confidence: field.confidence >= 0.9 ? 'high' : field.confidence >= 0.7 ? 'medium' : 'low',
|
|
583
|
+
doubt: field.confidence < 0.7 ? 'Confiance faible dans l\'extraction' : null,
|
|
584
|
+
wasEmpty: !wasFilled,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
// Identifier les champs non remplis
|
|
588
|
+
const filledTypes = new Set(sourceData.fields.map(f => f.fieldType));
|
|
589
|
+
const expectedFields = ['nom_commercial', 'siret', 'adresse', 'email', 'telephone'];
|
|
590
|
+
const unfilledFields = expectedFields.filter(f => !filledTypes.has(f));
|
|
591
|
+
if (unfilledFields.length > 0) {
|
|
592
|
+
warnings.push(`Champs attendus non trouvés: ${unfilledFields.join(', ')}`);
|
|
593
|
+
}
|
|
594
|
+
// Vérifier la compatibilité
|
|
595
|
+
const compatiblePairs = [
|
|
596
|
+
['DC1', 'DC1'], ['DC1', 'DC2'], ['DC2', 'DC1'], ['DC2', 'DC2'],
|
|
597
|
+
['AE', 'AE'], ['AE', 'DC1'], ['AE', 'DC2'],
|
|
598
|
+
['DC1', 'AE'], ['DC2', 'AE'],
|
|
599
|
+
];
|
|
600
|
+
const isCompatible = compatiblePairs.some(([s, t]) => sourceData.documentType.includes(s) && templateDocType.includes(t)) || sourceData.documentType === 'unknown' || templateDocType === 'unknown';
|
|
601
|
+
if (!isCompatible) {
|
|
602
|
+
warnings.push(`Types de documents potentiellement incompatibles: ${sourceData.documentType} → ${templateDocType}`);
|
|
603
|
+
}
|
|
604
|
+
// Suggestions
|
|
605
|
+
if (sourceData.fields.length < 3) {
|
|
606
|
+
suggestions.push('Peu de champs extraits. Vérifiez que le document source contient bien les données entreprise.');
|
|
607
|
+
}
|
|
608
|
+
if (fillPositions.length > sourceData.fields.length) {
|
|
609
|
+
suggestions.push('Le template contient plus de champs que le source. Certains resteront vides.');
|
|
610
|
+
}
|
|
611
|
+
const highConfCount = fieldDetails.filter(f => f.confidence === 'high').length;
|
|
612
|
+
const medConfCount = fieldDetails.filter(f => f.confidence === 'medium').length;
|
|
613
|
+
const lowConfCount = fieldDetails.filter(f => f.confidence === 'low').length;
|
|
614
|
+
return {
|
|
615
|
+
documentsCompatible: isCompatible,
|
|
616
|
+
compatibilityScore: isCompatible ? (sourceData.fields.length > 3 ? 85 : 60) : 30,
|
|
617
|
+
compatibilityReason: isCompatible
|
|
618
|
+
? 'Documents compatibles pour le mapping des données entreprise'
|
|
619
|
+
: 'Types de documents potentiellement incompatibles',
|
|
620
|
+
sourceDocType: sourceData.documentType,
|
|
621
|
+
templateDocType,
|
|
622
|
+
fieldsAnalysis: {
|
|
623
|
+
total: fillPositions.length,
|
|
624
|
+
filled: filledFields.length,
|
|
625
|
+
unfilled: fillPositions.length - filledFields.length,
|
|
626
|
+
highConfidence: highConfCount,
|
|
627
|
+
mediumConfidence: medConfCount,
|
|
628
|
+
lowConfidence: lowConfCount,
|
|
629
|
+
},
|
|
630
|
+
fieldDetails,
|
|
631
|
+
checkboxesAnalysis: {
|
|
632
|
+
total: sourceData.checkboxes.length,
|
|
633
|
+
checked: sourceData.checkboxes.filter(c => c.isChecked).length,
|
|
634
|
+
unchecked: sourceData.checkboxes.filter(c => !c.isChecked).length,
|
|
635
|
+
},
|
|
636
|
+
warnings,
|
|
637
|
+
suggestions,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Effectue une vérification avec le LLM
|
|
642
|
+
*/
|
|
643
|
+
async function performLLMVerification(llm, sourceText, templateText, sourceDocType, templateDocType, mappedFields) {
|
|
644
|
+
var _a, _b, _c, _d, _e, _f;
|
|
645
|
+
const prompt = VERIFICATION_PROMPT
|
|
646
|
+
.replace('{source_type}', sourceDocType)
|
|
647
|
+
.replace('{template_type}', templateDocType)
|
|
648
|
+
.replace('{source_text}', sourceText.substring(0, 3000))
|
|
649
|
+
.replace('{template_text}', templateText.substring(0, 3000))
|
|
650
|
+
.replace('{mapped_fields}', JSON.stringify(mappedFields, null, 2));
|
|
651
|
+
try {
|
|
652
|
+
const response = await llm.invoke(prompt);
|
|
653
|
+
const responseText = typeof response === 'string'
|
|
654
|
+
? response
|
|
655
|
+
: response.content || response.text || JSON.stringify(response);
|
|
656
|
+
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
|
657
|
+
if (jsonMatch) {
|
|
658
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
659
|
+
return {
|
|
660
|
+
documentsCompatible: (_a = parsed.documentsCompatible) !== null && _a !== void 0 ? _a : true,
|
|
661
|
+
compatibilityScore: (_b = parsed.compatibilityScore) !== null && _b !== void 0 ? _b : 70,
|
|
662
|
+
compatibilityReason: (_c = parsed.compatibilityReason) !== null && _c !== void 0 ? _c : 'Analyse IA effectuée',
|
|
663
|
+
warnings: (_d = parsed.warnings) !== null && _d !== void 0 ? _d : [],
|
|
664
|
+
suggestions: (_e = parsed.suggestions) !== null && _e !== void 0 ? _e : [],
|
|
665
|
+
fieldDetails: ((_f = parsed.fieldVerifications) !== null && _f !== void 0 ? _f : []).map((fv) => ({
|
|
666
|
+
fieldType: fv.fieldType,
|
|
667
|
+
label: fv.label,
|
|
668
|
+
filledValue: fv.filledValue,
|
|
669
|
+
confidence: fv.confidence || 'medium',
|
|
670
|
+
doubt: fv.doubt,
|
|
671
|
+
wasEmpty: false,
|
|
672
|
+
})),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
// En cas d'erreur, retourner un rapport partiel
|
|
678
|
+
return {
|
|
679
|
+
warnings: [`Erreur lors de la vérification IA: ${error.message}`],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return {};
|
|
683
|
+
}
|
|
533
684
|
/**
|
|
534
685
|
* Convertit le document en texte structuré pour le LLM
|
|
535
686
|
*/
|
|
@@ -609,6 +760,68 @@ RÈGLES:
|
|
|
609
760
|
1. Fais le mapping SÉMANTIQUE (Raison sociale = Dénomination = Nom commercial)
|
|
610
761
|
2. Utilise les [EMPTY] du template comme positions cibles
|
|
611
762
|
3. Le templateIndex doit correspondre à un paragraphe [EMPTY] qui suit un [LABEL]`;
|
|
763
|
+
const VERIFICATION_PROMPT = `Tu es un expert en vérification de documents administratifs français (DC1, DC2, AE, ATTRI1).
|
|
764
|
+
|
|
765
|
+
CONTEXTE:
|
|
766
|
+
- Document SOURCE (contient les données de l'entreprise): {source_type}
|
|
767
|
+
- Document TEMPLATE (formulaire à remplir): {template_type}
|
|
768
|
+
|
|
769
|
+
DOCUMENT SOURCE (avec données):
|
|
770
|
+
"""
|
|
771
|
+
{source_text}
|
|
772
|
+
"""
|
|
773
|
+
|
|
774
|
+
DOCUMENT TEMPLATE (à remplir):
|
|
775
|
+
"""
|
|
776
|
+
{template_text}
|
|
777
|
+
"""
|
|
778
|
+
|
|
779
|
+
DONNÉES EXTRAITES ET MAPPÉES:
|
|
780
|
+
{mapped_fields}
|
|
781
|
+
|
|
782
|
+
ANALYSE DEMANDÉE:
|
|
783
|
+
1. Les deux documents sont-ils compatibles pour un mapping de données entreprise ?
|
|
784
|
+
- Un DC1/DC2/AE rempli peut servir de source pour un autre DC1/DC2/AE vide
|
|
785
|
+
- Les données d'entreprise (SIRET, adresse, nom, etc.) sont transférables entre formulaires similaires
|
|
786
|
+
|
|
787
|
+
2. Pour chaque champ mappé, évalue:
|
|
788
|
+
- La CONFIANCE du mapping (high/medium/low)
|
|
789
|
+
- Un éventuel DOUTE si le mapping semble incorrect
|
|
790
|
+
|
|
791
|
+
3. Identifie:
|
|
792
|
+
- Les champs qui DEVRAIENT être remplis mais ne le sont pas
|
|
793
|
+
- Les WARNINGS (incohérences potentielles)
|
|
794
|
+
- Les SUGGESTIONS d'amélioration
|
|
795
|
+
|
|
796
|
+
IMPORTANT - Distingue bien:
|
|
797
|
+
- Données de l'ACHETEUR (nom du marché, références, dates limites) = NE PAS mapper
|
|
798
|
+
- Données du CANDIDAT (entreprise: SIRET, adresse, nom commercial, etc.) = À mapper
|
|
799
|
+
|
|
800
|
+
Retourne UNIQUEMENT un JSON valide:
|
|
801
|
+
{
|
|
802
|
+
"documentsCompatible": true,
|
|
803
|
+
"compatibilityScore": 85,
|
|
804
|
+
"compatibilityReason": "Les deux documents sont des formulaires de marchés publics partageant les mêmes champs entreprise",
|
|
805
|
+
"fieldVerifications": [
|
|
806
|
+
{
|
|
807
|
+
"fieldType": "siret",
|
|
808
|
+
"label": "Numéro SIRET",
|
|
809
|
+
"filledValue": "89198692900018",
|
|
810
|
+
"confidence": "high",
|
|
811
|
+
"doubt": null
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
"fieldType": "adresse",
|
|
815
|
+
"label": "Adresse postale",
|
|
816
|
+
"filledValue": "13 rue exemple",
|
|
817
|
+
"confidence": "medium",
|
|
818
|
+
"doubt": "L'adresse semble incomplète (code postal manquant)"
|
|
819
|
+
}
|
|
820
|
+
],
|
|
821
|
+
"unfilledFields": ["capital", "code_naf"],
|
|
822
|
+
"warnings": ["Le SIRET source ne correspond pas au format attendu"],
|
|
823
|
+
"suggestions": ["Vérifier manuellement le champ TVA intracommunautaire"]
|
|
824
|
+
}`;
|
|
612
825
|
// ============================================================================
|
|
613
826
|
// Main Node Class
|
|
614
827
|
// ============================================================================
|
|
@@ -738,6 +951,14 @@ class DocxFillerAI {
|
|
|
738
951
|
displayOptions: { show: { operation: ['fill'] } },
|
|
739
952
|
description: 'Utiliser le LLM connecté pour un mapping plus intelligent (plus lent)',
|
|
740
953
|
},
|
|
954
|
+
{
|
|
955
|
+
displayName: 'AI Verification',
|
|
956
|
+
name: 'enableVerification',
|
|
957
|
+
type: 'boolean',
|
|
958
|
+
default: true,
|
|
959
|
+
displayOptions: { show: { operation: ['fill'] } },
|
|
960
|
+
description: 'Activer la vérification IA pour valider le mapping et détecter les doutes',
|
|
961
|
+
},
|
|
741
962
|
// Extract
|
|
742
963
|
{
|
|
743
964
|
displayName: 'Document Input Type',
|
|
@@ -789,6 +1010,7 @@ class DocxFillerAI {
|
|
|
789
1010
|
const templateValue = this.getNodeParameter('templateDocument', i, '');
|
|
790
1011
|
const outputProperty = this.getNodeParameter('outputProperty', i);
|
|
791
1012
|
const useLLM = this.getNodeParameter('useLLM', i);
|
|
1013
|
+
const enableVerification = this.getNodeParameter('enableVerification', i);
|
|
792
1014
|
// Charger les documents
|
|
793
1015
|
const sourceBuffer = await getDocumentBuffer(this, i, sourceInputType, sourceValue, items, 'source');
|
|
794
1016
|
const templateBuffer = await getDocumentBuffer(this, i, templateInputType, templateValue, items, 'template');
|
|
@@ -854,22 +1076,87 @@ class DocxFillerAI {
|
|
|
854
1076
|
type: 'nodebuffer',
|
|
855
1077
|
compression: 'DEFLATE',
|
|
856
1078
|
});
|
|
1079
|
+
// Détecter le type du template (document de sortie)
|
|
1080
|
+
const templateDocType = detectDocumentType(templateParagraphs);
|
|
1081
|
+
const finalDocType = templateDocType !== 'unknown' ? templateDocType : sourceData.documentType;
|
|
1082
|
+
// Générer le rapport de vérification
|
|
1083
|
+
const fillPositionsForReport = findFillPositions(templateParagraphs);
|
|
1084
|
+
let verificationReport = generateBasicVerificationReport(sourceData, finalDocType, filledFields, fillPositionsForReport);
|
|
1085
|
+
// Si vérification IA activée et LLM disponible, enrichir le rapport
|
|
1086
|
+
if (enableVerification && llm) {
|
|
1087
|
+
const sourceStructuredText = docxToStructuredText(sourceParagraphs);
|
|
1088
|
+
const templateStructuredText = docxToStructuredText(templateParagraphs);
|
|
1089
|
+
const llmVerification = await performLLMVerification(llm, sourceStructuredText, templateStructuredText, sourceData.documentType, finalDocType, filledFields);
|
|
1090
|
+
// Fusionner les résultats LLM avec le rapport basique
|
|
1091
|
+
if (llmVerification.documentsCompatible !== undefined) {
|
|
1092
|
+
verificationReport.documentsCompatible = llmVerification.documentsCompatible;
|
|
1093
|
+
}
|
|
1094
|
+
if (llmVerification.compatibilityScore !== undefined) {
|
|
1095
|
+
verificationReport.compatibilityScore = llmVerification.compatibilityScore;
|
|
1096
|
+
}
|
|
1097
|
+
if (llmVerification.compatibilityReason) {
|
|
1098
|
+
verificationReport.compatibilityReason = llmVerification.compatibilityReason;
|
|
1099
|
+
}
|
|
1100
|
+
if (llmVerification.warnings && llmVerification.warnings.length > 0) {
|
|
1101
|
+
verificationReport.warnings = [...verificationReport.warnings, ...llmVerification.warnings];
|
|
1102
|
+
}
|
|
1103
|
+
if (llmVerification.suggestions && llmVerification.suggestions.length > 0) {
|
|
1104
|
+
verificationReport.suggestions = [...verificationReport.suggestions, ...llmVerification.suggestions];
|
|
1105
|
+
}
|
|
1106
|
+
if (llmVerification.fieldDetails && llmVerification.fieldDetails.length > 0) {
|
|
1107
|
+
// Enrichir avec les détails LLM (doutes, confiance)
|
|
1108
|
+
for (const llmField of llmVerification.fieldDetails) {
|
|
1109
|
+
const existingField = verificationReport.fieldDetails.find(f => f.fieldType === llmField.fieldType);
|
|
1110
|
+
if (existingField && llmField.doubt) {
|
|
1111
|
+
existingField.doubt = llmField.doubt;
|
|
1112
|
+
existingField.confidence = llmField.confidence;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
857
1117
|
// Générer le nom de fichier
|
|
858
1118
|
const date = new Date().toISOString().split('T')[0];
|
|
859
1119
|
const companyName = (sourceData.companyName || 'Document').replace(/[^a-zA-Z0-9]/g, '_');
|
|
860
|
-
const outputFilename = `${companyName}_${
|
|
1120
|
+
const outputFilename = `${companyName}_${finalDocType}_${date}.docx`;
|
|
861
1121
|
const binaryData = await this.helpers.prepareBinaryData(outputBuffer, outputFilename, DOCX_MIME_TYPE);
|
|
1122
|
+
// Extraire les champs avec doutes pour un accès facile
|
|
1123
|
+
const fieldsWithDoubts = verificationReport.fieldDetails
|
|
1124
|
+
.filter(f => f.doubt !== null)
|
|
1125
|
+
.map(f => ({ field: f.fieldType, label: f.label, doubt: f.doubt }));
|
|
862
1126
|
returnData.push({
|
|
863
1127
|
json: {
|
|
864
1128
|
success: true,
|
|
865
1129
|
filename: outputFilename,
|
|
866
|
-
documentType:
|
|
1130
|
+
documentType: finalDocType,
|
|
1131
|
+
sourceDocumentType: sourceData.documentType,
|
|
867
1132
|
companyName: sourceData.companyName,
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1133
|
+
// Résumé du remplissage
|
|
1134
|
+
summary: {
|
|
1135
|
+
filledFields: filledFields.length,
|
|
1136
|
+
unfilledFields: verificationReport.fieldsAnalysis.unfilled,
|
|
1137
|
+
modifiedCheckboxes,
|
|
1138
|
+
extractedFieldsCount: sourceData.fields.length,
|
|
1139
|
+
compatibilityScore: verificationReport.compatibilityScore,
|
|
1140
|
+
},
|
|
1141
|
+
// Détails des champs remplis
|
|
1142
|
+
filledFieldsList: filledFields,
|
|
1143
|
+
// Rapport de vérification complet
|
|
1144
|
+
verification: {
|
|
1145
|
+
documentsCompatible: verificationReport.documentsCompatible,
|
|
1146
|
+
compatibilityScore: verificationReport.compatibilityScore,
|
|
1147
|
+
compatibilityReason: verificationReport.compatibilityReason,
|
|
1148
|
+
fieldsAnalysis: verificationReport.fieldsAnalysis,
|
|
1149
|
+
checkboxesAnalysis: verificationReport.checkboxesAnalysis,
|
|
1150
|
+
fieldsWithDoubts,
|
|
1151
|
+
warnings: verificationReport.warnings,
|
|
1152
|
+
suggestions: verificationReport.suggestions,
|
|
1153
|
+
},
|
|
1154
|
+
// Détails complets des champs (pour debug)
|
|
1155
|
+
fieldDetails: verificationReport.fieldDetails,
|
|
1156
|
+
// Métadonnées
|
|
871
1157
|
usedLLM: useLLM && !!llm,
|
|
872
|
-
|
|
1158
|
+
usedVerification: enableVerification && !!llm,
|
|
1159
|
+
message: `Rempli: ${filledFields.length} champs, ${modifiedCheckboxes} checkboxes. Score: ${verificationReport.compatibilityScore}%`,
|
|
873
1160
|
},
|
|
874
1161
|
binary: {
|
|
875
1162
|
[outputProperty]: binaryData,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-docx-filler",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "n8n node to automatically fill DOCX documents (French DC1, DC2, AE forms) using AI for semantic understanding and field mapping.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|