n8n-nodes-docx-filler 1.1.0 → 2.0.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.
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.DocxFillerAI = void 0;
|
|
40
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
41
|
+
const pizzip_1 = __importDefault(require("pizzip"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Constants
|
|
45
|
+
// ============================================================================
|
|
46
|
+
const DOCX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
47
|
+
const DOCX_MIME_TYPES = [
|
|
48
|
+
DOCX_MIME_TYPE,
|
|
49
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
|
50
|
+
'application/msword',
|
|
51
|
+
];
|
|
52
|
+
const CHECKBOX_UNCHECKED = ['☐', '□', '▢'];
|
|
53
|
+
const CHECKBOX_CHECKED = ['☒', '☑', '▣'];
|
|
54
|
+
// Patterns sémantiques pour la détection des champs DC1/DC2/AE
|
|
55
|
+
const FIELD_PATTERNS = [
|
|
56
|
+
{
|
|
57
|
+
id: 'nom_commercial',
|
|
58
|
+
patterns: ['nom commercial', 'dénomination sociale', 'raison sociale', 'établissement qui exécutera'],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'siret',
|
|
62
|
+
patterns: ['siret', 'numéro siret', 'n° siret'],
|
|
63
|
+
valuePatterns: [/^\d{9,14}$/, /^\d{3}\s?\d{3}\s?\d{3}\s?\d{5}$/],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'adresse',
|
|
67
|
+
patterns: ['adresse postale', 'siège social', 'adresses postale'],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'email',
|
|
71
|
+
patterns: ['adresse électronique', 'courriel', 'e-mail', 'email'],
|
|
72
|
+
valuePatterns: [/^[\w.-]+@[\w.-]+\.\w+$/],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'telephone',
|
|
76
|
+
patterns: ['téléphone', 'télécopie', 'numéros de téléphone'],
|
|
77
|
+
valuePatterns: [/^[\+\d\s\.\-\(\)]{8,}$/],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'tva_intra',
|
|
81
|
+
patterns: ['tva intracommunautaire', 'identification européen', 'numéro tva'],
|
|
82
|
+
valuePatterns: [/^FR\s?\d{2}\s?\d{9}$/i],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'forme_juridique',
|
|
86
|
+
patterns: ['forme juridique', 'statut juridique'],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'capital',
|
|
90
|
+
patterns: ['capital social'],
|
|
91
|
+
valuePatterns: [/^\d+[\s\d]*€?$/],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'code_naf',
|
|
95
|
+
patterns: ['code naf', 'code ape'],
|
|
96
|
+
valuePatterns: [/^\d{4}[A-Z]$/],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'pme',
|
|
100
|
+
patterns: ['micro, une petite ou une moyenne entreprise', 'pme', 'petite ou moyenne'],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Helper Functions
|
|
105
|
+
// ============================================================================
|
|
106
|
+
function hasCheckbox(text) {
|
|
107
|
+
return [...CHECKBOX_UNCHECKED, ...CHECKBOX_CHECKED].some(c => text.includes(c));
|
|
108
|
+
}
|
|
109
|
+
function isChecked(text) {
|
|
110
|
+
return CHECKBOX_CHECKED.some(c => text.includes(c));
|
|
111
|
+
}
|
|
112
|
+
function replaceCheckboxState(text, checked) {
|
|
113
|
+
let result = text;
|
|
114
|
+
if (checked) {
|
|
115
|
+
for (const c of CHECKBOX_UNCHECKED) {
|
|
116
|
+
result = result.split(c).join('☒');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
for (const c of CHECKBOX_CHECKED) {
|
|
121
|
+
result = result.split(c).join('☐');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
function normalize(text) {
|
|
127
|
+
return text
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.replace(/[■▪●○•\s]+/g, ' ')
|
|
130
|
+
.replace(/\s+/g, ' ')
|
|
131
|
+
.trim();
|
|
132
|
+
}
|
|
133
|
+
function escapeXml(text) {
|
|
134
|
+
return text
|
|
135
|
+
.replace(/&/g, '&')
|
|
136
|
+
.replace(/</g, '<')
|
|
137
|
+
.replace(/>/g, '>')
|
|
138
|
+
.replace(/"/g, '"')
|
|
139
|
+
.replace(/'/g, ''');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Extrait les paragraphes avec leurs positions dans le XML
|
|
143
|
+
*/
|
|
144
|
+
function extractParagraphsWithPositions(xml) {
|
|
145
|
+
const results = [];
|
|
146
|
+
const pRegex = /<w:p[^>]*>([\s\S]*?)<\/w:p>/g;
|
|
147
|
+
let match;
|
|
148
|
+
let index = 0;
|
|
149
|
+
while ((match = pRegex.exec(xml)) !== null) {
|
|
150
|
+
const pContent = match[1];
|
|
151
|
+
const textParts = [];
|
|
152
|
+
const tRegex = /<w:t[^>]*>([^<]*)<\/w:t>/g;
|
|
153
|
+
let tMatch;
|
|
154
|
+
while ((tMatch = tRegex.exec(pContent)) !== null) {
|
|
155
|
+
textParts.push(tMatch[1]);
|
|
156
|
+
}
|
|
157
|
+
const text = textParts.join('');
|
|
158
|
+
const textNormalized = normalize(text);
|
|
159
|
+
const trimmedText = text.trim();
|
|
160
|
+
results.push({
|
|
161
|
+
index,
|
|
162
|
+
text,
|
|
163
|
+
textNormalized,
|
|
164
|
+
fullMatch: match[0],
|
|
165
|
+
start: match.index,
|
|
166
|
+
end: match.index + match[0].length,
|
|
167
|
+
isEmpty: !trimmedText || trimmedText.length < 3,
|
|
168
|
+
isLabel: trimmedText.startsWith('■') || trimmedText.startsWith('▪') || trimmedText.startsWith(' '),
|
|
169
|
+
hasCheckbox: hasCheckbox(text),
|
|
170
|
+
isChecked: isChecked(text),
|
|
171
|
+
});
|
|
172
|
+
index++;
|
|
173
|
+
}
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Détecte si un paragraphe est un label pour un type de champ donné
|
|
178
|
+
*/
|
|
179
|
+
function detectFieldType(text) {
|
|
180
|
+
const normalized = normalize(text);
|
|
181
|
+
for (const field of FIELD_PATTERNS) {
|
|
182
|
+
if (field.patterns.some(pattern => normalized.includes(pattern))) {
|
|
183
|
+
return field.id;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Valide si une valeur correspond au type de champ attendu
|
|
190
|
+
*/
|
|
191
|
+
function validateFieldValue(fieldType, value) {
|
|
192
|
+
const field = FIELD_PATTERNS.find(f => f.id === fieldType);
|
|
193
|
+
if (!field || !field.valuePatterns) {
|
|
194
|
+
// Pas de pattern spécifique, accepter si la valeur semble valide
|
|
195
|
+
return value.length >= 2 && value.length <= 500;
|
|
196
|
+
}
|
|
197
|
+
const cleanValue = value.trim();
|
|
198
|
+
return field.valuePatterns.some(pattern => pattern.test(cleanValue));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Extrait les données d'un document source
|
|
202
|
+
*/
|
|
203
|
+
function extractSourceData(paragraphs) {
|
|
204
|
+
const fields = [];
|
|
205
|
+
const checkboxes = [];
|
|
206
|
+
let documentType = 'unknown';
|
|
207
|
+
let companyName = null;
|
|
208
|
+
const usedValueIndices = new Set();
|
|
209
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
210
|
+
const p = paragraphs[i];
|
|
211
|
+
const text = p.text.trim();
|
|
212
|
+
// Détecter le type de document
|
|
213
|
+
if (p.textNormalized.includes('dc1') || p.textNormalized.includes('lettre de candidature')) {
|
|
214
|
+
documentType = 'DC1';
|
|
215
|
+
}
|
|
216
|
+
else if (p.textNormalized.includes('dc2') || p.textNormalized.includes('déclaration du candidat')) {
|
|
217
|
+
documentType = 'DC2';
|
|
218
|
+
}
|
|
219
|
+
// Extraire les checkboxes
|
|
220
|
+
if (p.hasCheckbox) {
|
|
221
|
+
checkboxes.push({
|
|
222
|
+
text,
|
|
223
|
+
signature: normalize(text.replace(/[☐☒☑□▢▣]/g, '')),
|
|
224
|
+
isChecked: p.isChecked,
|
|
225
|
+
paragraphIndex: i,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Détecter si c'est un label
|
|
229
|
+
const fieldType = detectFieldType(text);
|
|
230
|
+
if (fieldType) {
|
|
231
|
+
// Chercher la valeur dans les paragraphes suivants
|
|
232
|
+
for (let j = i + 1; j < Math.min(i + 6, paragraphs.length); j++) {
|
|
233
|
+
if (usedValueIndices.has(j))
|
|
234
|
+
continue;
|
|
235
|
+
const nextP = paragraphs[j];
|
|
236
|
+
const nextText = nextP.text.trim();
|
|
237
|
+
// Ignorer les paragraphes vides
|
|
238
|
+
if (!nextText || nextText.length < 2)
|
|
239
|
+
continue;
|
|
240
|
+
// Si c'est un autre label, arrêter
|
|
241
|
+
if (detectFieldType(nextText))
|
|
242
|
+
break;
|
|
243
|
+
if (nextText.startsWith('■') || nextText.startsWith('▪'))
|
|
244
|
+
break;
|
|
245
|
+
// Vérifier que ce n'est pas un label connu
|
|
246
|
+
const isNextLabel = FIELD_PATTERNS.some(f => f.patterns.some(pat => normalize(nextText).includes(pat)));
|
|
247
|
+
if (isNextLabel)
|
|
248
|
+
break;
|
|
249
|
+
// C'est potentiellement une valeur
|
|
250
|
+
// Vérifier si elle correspond au type attendu ou si elle semble valide
|
|
251
|
+
const isValid = validateFieldValue(fieldType, nextText) ||
|
|
252
|
+
(nextText.length >= 3 && nextText.length <= 200 && !nextP.hasCheckbox);
|
|
253
|
+
if (isValid) {
|
|
254
|
+
fields.push({
|
|
255
|
+
fieldType,
|
|
256
|
+
label: text.slice(0, 60),
|
|
257
|
+
value: nextText,
|
|
258
|
+
labelIndex: i,
|
|
259
|
+
valueIndex: j,
|
|
260
|
+
confidence: validateFieldValue(fieldType, nextText) ? 0.9 : 0.7,
|
|
261
|
+
});
|
|
262
|
+
usedValueIndices.add(j);
|
|
263
|
+
// Capturer le nom de l'entreprise
|
|
264
|
+
if (fieldType === 'nom_commercial' && !companyName) {
|
|
265
|
+
companyName = nextText;
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { documentType, companyName, fields, checkboxes };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Trouve les positions à remplir dans un template
|
|
276
|
+
*/
|
|
277
|
+
function findFillPositions(paragraphs) {
|
|
278
|
+
const positions = [];
|
|
279
|
+
const usedFillIndices = new Set();
|
|
280
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
281
|
+
const p = paragraphs[i];
|
|
282
|
+
const fieldType = detectFieldType(p.text);
|
|
283
|
+
if (fieldType) {
|
|
284
|
+
// Chercher le prochain paragraphe vide pour y insérer la valeur
|
|
285
|
+
for (let j = i + 1; j < Math.min(i + 6, paragraphs.length); j++) {
|
|
286
|
+
if (usedFillIndices.has(j))
|
|
287
|
+
continue;
|
|
288
|
+
const nextP = paragraphs[j];
|
|
289
|
+
// Position vide = remplissable
|
|
290
|
+
if (nextP.isEmpty) {
|
|
291
|
+
positions.push({
|
|
292
|
+
fieldType,
|
|
293
|
+
labelIndex: i,
|
|
294
|
+
labelText: p.text.slice(0, 60),
|
|
295
|
+
fillIndex: j,
|
|
296
|
+
paragraph: nextP,
|
|
297
|
+
});
|
|
298
|
+
usedFillIndices.add(j);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
// Si on trouve un autre label, arrêter
|
|
302
|
+
if (detectFieldType(nextP.text))
|
|
303
|
+
break;
|
|
304
|
+
if (nextP.text.trim().startsWith('■') || nextP.text.trim().startsWith('▪'))
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return positions;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Remplit un template avec les données source
|
|
313
|
+
*/
|
|
314
|
+
function fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions) {
|
|
315
|
+
let xml = templateXml;
|
|
316
|
+
const filledFields = [];
|
|
317
|
+
let modifiedCheckboxes = 0;
|
|
318
|
+
// Créer un map des données source par type de champ
|
|
319
|
+
const sourceFieldMap = new Map();
|
|
320
|
+
for (const field of sourceData.fields) {
|
|
321
|
+
if (!sourceFieldMap.has(field.fieldType)) {
|
|
322
|
+
sourceFieldMap.set(field.fieldType, field);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Collecter toutes les modifications à faire
|
|
326
|
+
const modifications = [];
|
|
327
|
+
for (const pos of fillPositions) {
|
|
328
|
+
const sourceField = sourceFieldMap.get(pos.fieldType);
|
|
329
|
+
if (!sourceField)
|
|
330
|
+
continue;
|
|
331
|
+
const oldP = pos.paragraph.fullMatch;
|
|
332
|
+
// Créer un nouveau paragraphe avec la valeur, en conservant la structure
|
|
333
|
+
const newP = oldP.replace(/(<w:p[^>]*>)([\s\S]*?)(<\/w:p>)/, `$1<w:r><w:t>${escapeXml(sourceField.value)}</w:t></w:r>$3`);
|
|
334
|
+
modifications.push({
|
|
335
|
+
start: pos.paragraph.start,
|
|
336
|
+
end: pos.paragraph.end,
|
|
337
|
+
newContent: newP,
|
|
338
|
+
fieldType: pos.fieldType,
|
|
339
|
+
value: sourceField.value,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// Appliquer les modifications en ordre inverse (pour éviter les décalages)
|
|
343
|
+
modifications.sort((a, b) => b.start - a.start);
|
|
344
|
+
for (const mod of modifications) {
|
|
345
|
+
xml = xml.slice(0, mod.start) + mod.newContent + xml.slice(mod.end);
|
|
346
|
+
filledFields.push(`${mod.fieldType}: ${mod.value}`);
|
|
347
|
+
}
|
|
348
|
+
// Synchroniser les checkboxes
|
|
349
|
+
for (const templateP of templateParagraphs) {
|
|
350
|
+
if (!templateP.hasCheckbox)
|
|
351
|
+
continue;
|
|
352
|
+
const tSignature = normalize(templateP.text.replace(/[☐☒☑□▢▣]/g, ''));
|
|
353
|
+
for (const sourceCb of sourceData.checkboxes) {
|
|
354
|
+
// Correspondance par signature similaire
|
|
355
|
+
if (tSignature.includes(sourceCb.signature.slice(0, 30)) ||
|
|
356
|
+
sourceCb.signature.includes(tSignature.slice(0, 30))) {
|
|
357
|
+
if (sourceCb.isChecked !== templateP.isChecked) {
|
|
358
|
+
const newText = replaceCheckboxState(templateP.text, sourceCb.isChecked);
|
|
359
|
+
const escapedOld = escapeXml(templateP.text);
|
|
360
|
+
const escapedNew = escapeXml(newText);
|
|
361
|
+
if (xml.includes(escapedOld)) {
|
|
362
|
+
xml = xml.split(escapedOld).join(escapedNew);
|
|
363
|
+
modifiedCheckboxes++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { xml, filledFields, modifiedCheckboxes };
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Valide qu'un buffer est un fichier DOCX valide
|
|
374
|
+
*/
|
|
375
|
+
function validateDocxBuffer(buffer, source) {
|
|
376
|
+
if (buffer.length < 4 || buffer[0] !== 0x50 || buffer[1] !== 0x4B) {
|
|
377
|
+
throw new Error(`Le fichier "${source}" n'est pas un DOCX valide. ` +
|
|
378
|
+
`Un DOCX doit être un fichier ZIP (signature PK). ` +
|
|
379
|
+
`Vérifiez que le fichier n'est pas corrompu.`);
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const zip = new pizzip_1.default(buffer);
|
|
383
|
+
const docXml = zip.file('word/document.xml');
|
|
384
|
+
if (!docXml) {
|
|
385
|
+
throw new Error(`Le fichier "${source}" est un ZIP mais pas un DOCX valide. ` +
|
|
386
|
+
`Assurez-vous d'utiliser un document Word (.docx), pas un .doc.`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (zipError) {
|
|
390
|
+
if (zipError.message.includes('word/document.xml')) {
|
|
391
|
+
throw zipError;
|
|
392
|
+
}
|
|
393
|
+
throw new Error(`Erreur lors de la lecture du fichier "${source}": ${zipError.message}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Charge un document DOCX depuis différentes sources
|
|
398
|
+
*/
|
|
399
|
+
async function getDocumentBuffer(context, itemIndex, inputType, inputValue, items, documentName) {
|
|
400
|
+
let buffer = null;
|
|
401
|
+
let source = inputValue;
|
|
402
|
+
// Mode binaire
|
|
403
|
+
if (inputType === 'binary' || inputType === 'auto') {
|
|
404
|
+
if (items[itemIndex].binary && items[itemIndex].binary[inputValue]) {
|
|
405
|
+
const binaryData = items[itemIndex].binary[inputValue];
|
|
406
|
+
if (binaryData.mimeType && !DOCX_MIME_TYPES.includes(binaryData.mimeType)) {
|
|
407
|
+
if (inputType === 'binary') {
|
|
408
|
+
throw new Error(`Le fichier binaire "${inputValue}" n'est pas un DOCX. ` +
|
|
409
|
+
`Type MIME: ${binaryData.mimeType}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
buffer = await context.helpers.getBinaryDataBuffer(itemIndex, inputValue);
|
|
414
|
+
source = binaryData.fileName || inputValue;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (inputType === 'binary') {
|
|
418
|
+
const availableBinaries = items[itemIndex].binary
|
|
419
|
+
? Object.keys(items[itemIndex].binary)
|
|
420
|
+
: [];
|
|
421
|
+
throw new Error(`Propriété binaire "${inputValue}" non trouvée pour ${documentName}. ` +
|
|
422
|
+
`Disponibles: ${availableBinaries.length > 0 ? availableBinaries.join(', ') : 'aucune'}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Mode chemin
|
|
426
|
+
if (!buffer && (inputType === 'path' || inputType === 'auto')) {
|
|
427
|
+
const looksLikePath = inputValue.startsWith('/') ||
|
|
428
|
+
inputValue.startsWith('./') ||
|
|
429
|
+
inputValue.startsWith('../') ||
|
|
430
|
+
/^[a-zA-Z]:\\/.test(inputValue) ||
|
|
431
|
+
(inputValue.includes('.docx') && !inputValue.includes(' ') && inputValue.length < 500);
|
|
432
|
+
if (looksLikePath || inputType === 'path') {
|
|
433
|
+
try {
|
|
434
|
+
if (fs.existsSync(inputValue)) {
|
|
435
|
+
buffer = fs.readFileSync(inputValue);
|
|
436
|
+
source = inputValue;
|
|
437
|
+
}
|
|
438
|
+
else if (inputType === 'path') {
|
|
439
|
+
throw new Error(`Fichier non trouvé: "${inputValue}"`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch (fsError) {
|
|
443
|
+
if (inputType === 'path') {
|
|
444
|
+
throw new Error(`Impossible de lire "${inputValue}": ${fsError.message}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Mode base64
|
|
450
|
+
if (!buffer && inputType === 'auto') {
|
|
451
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
452
|
+
if (inputValue.length > 100 && base64Regex.test(inputValue.replace(/\s/g, ''))) {
|
|
453
|
+
try {
|
|
454
|
+
buffer = Buffer.from(inputValue, 'base64');
|
|
455
|
+
source = 'base64';
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Pas du base64 valide
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!buffer) {
|
|
463
|
+
throw new Error(`Impossible de charger ${documentName} depuis "${inputValue.substring(0, 50)}..."\n` +
|
|
464
|
+
`Formats acceptés: propriété binaire, chemin fichier, base64`);
|
|
465
|
+
}
|
|
466
|
+
validateDocxBuffer(buffer, source);
|
|
467
|
+
return buffer;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Convertit le document en texte structuré pour le LLM
|
|
471
|
+
*/
|
|
472
|
+
function docxToStructuredText(paragraphs) {
|
|
473
|
+
const lines = [];
|
|
474
|
+
for (const p of paragraphs) {
|
|
475
|
+
const text = p.text.trim();
|
|
476
|
+
if (!text)
|
|
477
|
+
continue;
|
|
478
|
+
const fieldType = detectFieldType(text);
|
|
479
|
+
if (p.hasCheckbox) {
|
|
480
|
+
const state = p.isChecked ? 'CHECKED' : 'UNCHECKED';
|
|
481
|
+
lines.push(`[${p.index}][CHECKBOX:${state}] ${text}`);
|
|
482
|
+
}
|
|
483
|
+
else if (fieldType) {
|
|
484
|
+
lines.push(`[${p.index}][LABEL:${fieldType}] ${text}`);
|
|
485
|
+
}
|
|
486
|
+
else if (p.isEmpty) {
|
|
487
|
+
lines.push(`[${p.index}][EMPTY]`);
|
|
488
|
+
}
|
|
489
|
+
else if (text.length > 0 && text.length < 200) {
|
|
490
|
+
lines.push(`[${p.index}][TEXT] ${text}`);
|
|
491
|
+
}
|
|
492
|
+
else if (text.length >= 200) {
|
|
493
|
+
lines.push(`[${p.index}][LONG] ${text.substring(0, 100)}...`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return lines.join('\n');
|
|
497
|
+
}
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// Prompts for LLM
|
|
500
|
+
// ============================================================================
|
|
501
|
+
const EXTRACTION_PROMPT = `Tu es un expert en analyse de documents administratifs français (DC1, DC2, AE, ATTRI1).
|
|
502
|
+
|
|
503
|
+
Analyse ce document et extrait TOUTES les données de l'entreprise.
|
|
504
|
+
|
|
505
|
+
DOCUMENT:
|
|
506
|
+
"""
|
|
507
|
+
{document_text}
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
Retourne UNIQUEMENT un JSON valide:
|
|
511
|
+
{
|
|
512
|
+
"documentType": "DC1|DC2|AE|autre",
|
|
513
|
+
"companyName": "nom de l'entreprise",
|
|
514
|
+
"fields": [
|
|
515
|
+
{"fieldType": "nom_commercial|siret|adresse|email|telephone|tva_intra|forme_juridique|capital|code_naf", "value": "valeur", "paragraphIndex": 123}
|
|
516
|
+
],
|
|
517
|
+
"checkboxes": [
|
|
518
|
+
{"text": "texte du checkbox", "isChecked": true, "paragraphIndex": 45}
|
|
519
|
+
]
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
IMPORTANT:
|
|
523
|
+
- Extrait TOUTES les valeurs remplies (SIRET, adresse, email, téléphone, etc.)
|
|
524
|
+
- Pour les checkboxes, isChecked=true si ☒☑▣, false si ☐□▢
|
|
525
|
+
- paragraphIndex = numéro entre crochets au début de chaque ligne`;
|
|
526
|
+
const FILL_PROMPT = `Tu dois mapper les données source vers les positions du template.
|
|
527
|
+
|
|
528
|
+
DONNÉES SOURCE:
|
|
529
|
+
{source_data}
|
|
530
|
+
|
|
531
|
+
TEMPLATE (positions vides à remplir):
|
|
532
|
+
{template_text}
|
|
533
|
+
|
|
534
|
+
Retourne UNIQUEMENT un JSON:
|
|
535
|
+
{
|
|
536
|
+
"mappings": [
|
|
537
|
+
{"sourceField": "nom_commercial", "templateIndex": 34, "value": "ROKODO.IO"}
|
|
538
|
+
],
|
|
539
|
+
"checkboxMappings": [
|
|
540
|
+
{"sourceSignature": "candidat pme", "templateIndex": 45, "shouldBeChecked": true}
|
|
541
|
+
]
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
RÈGLES:
|
|
545
|
+
1. Fais le mapping SÉMANTIQUE (Raison sociale = Dénomination = Nom commercial)
|
|
546
|
+
2. Utilise les [EMPTY] du template comme positions cibles
|
|
547
|
+
3. Le templateIndex doit correspondre à un paragraphe [EMPTY] qui suit un [LABEL]`;
|
|
548
|
+
// ============================================================================
|
|
549
|
+
// Main Node Class
|
|
550
|
+
// ============================================================================
|
|
551
|
+
class DocxFillerAI {
|
|
552
|
+
constructor() {
|
|
553
|
+
this.description = {
|
|
554
|
+
displayName: 'DOCX Filler AI',
|
|
555
|
+
name: 'docxFillerAi',
|
|
556
|
+
icon: 'file:docx.svg',
|
|
557
|
+
group: ['transform'],
|
|
558
|
+
version: 2,
|
|
559
|
+
subtitle: '={{$parameter["operation"] === "fill" ? "Remplir: " + $parameter["sourceInputType"] : "Extraire données"}}',
|
|
560
|
+
description: 'Remplit automatiquement des documents DOCX (DC1, DC2, AE) en utilisant l\'IA pour le mapping sémantique des champs.',
|
|
561
|
+
defaults: {
|
|
562
|
+
name: 'DOCX Filler AI',
|
|
563
|
+
},
|
|
564
|
+
inputs: [
|
|
565
|
+
{ displayName: '', type: 'main' },
|
|
566
|
+
{
|
|
567
|
+
displayName: 'Model',
|
|
568
|
+
maxConnections: 1,
|
|
569
|
+
type: 'ai_languageModel',
|
|
570
|
+
required: false, // Rendre optionnel pour permettre le mode sans LLM
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
outputs: [{ displayName: '', type: 'main' }],
|
|
574
|
+
properties: [
|
|
575
|
+
{
|
|
576
|
+
displayName: 'Operation',
|
|
577
|
+
name: 'operation',
|
|
578
|
+
type: 'options',
|
|
579
|
+
noDataExpression: true,
|
|
580
|
+
options: [
|
|
581
|
+
{
|
|
582
|
+
name: 'Fill Document',
|
|
583
|
+
value: 'fill',
|
|
584
|
+
description: 'Remplit un template avec les données d\'un document source',
|
|
585
|
+
action: 'Fill document',
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: 'Extract Data',
|
|
589
|
+
value: 'extract',
|
|
590
|
+
description: 'Extrait les données d\'un document rempli',
|
|
591
|
+
action: 'Extract data',
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
default: 'fill',
|
|
595
|
+
},
|
|
596
|
+
// Fill - Source
|
|
597
|
+
{
|
|
598
|
+
displayName: 'Source Input Type',
|
|
599
|
+
name: 'sourceInputType',
|
|
600
|
+
type: 'options',
|
|
601
|
+
displayOptions: { show: { operation: ['fill'] } },
|
|
602
|
+
options: [
|
|
603
|
+
{ name: 'Binary', value: 'binary', description: 'Propriété binaire' },
|
|
604
|
+
{ name: 'File Path', value: 'path', description: 'Chemin fichier' },
|
|
605
|
+
{ name: 'Auto-Detect', value: 'auto', description: 'Détection auto' },
|
|
606
|
+
],
|
|
607
|
+
default: 'auto',
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
displayName: 'Source Document',
|
|
611
|
+
name: 'sourceDocument',
|
|
612
|
+
type: 'string',
|
|
613
|
+
default: '',
|
|
614
|
+
required: true,
|
|
615
|
+
displayOptions: { show: { operation: ['fill'] } },
|
|
616
|
+
description: 'Document source (propriété binaire, chemin, ou base64)',
|
|
617
|
+
placeholder: 'sourceDoc ou /path/to/source.docx',
|
|
618
|
+
},
|
|
619
|
+
// Fill - Template
|
|
620
|
+
{
|
|
621
|
+
displayName: 'Template Input Type',
|
|
622
|
+
name: 'templateInputType',
|
|
623
|
+
type: 'options',
|
|
624
|
+
displayOptions: { show: { operation: ['fill'] } },
|
|
625
|
+
options: [
|
|
626
|
+
{ name: 'Binary', value: 'binary', description: 'Propriété binaire' },
|
|
627
|
+
{ name: 'File Path', value: 'path', description: 'Chemin fichier' },
|
|
628
|
+
{ name: 'Auto-Detect', value: 'auto', description: 'Détection auto' },
|
|
629
|
+
],
|
|
630
|
+
default: 'auto',
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
displayName: 'Template Document',
|
|
634
|
+
name: 'templateDocument',
|
|
635
|
+
type: 'string',
|
|
636
|
+
default: '',
|
|
637
|
+
required: true,
|
|
638
|
+
displayOptions: { show: { operation: ['fill'] } },
|
|
639
|
+
description: 'Template à remplir (propriété binaire, chemin, ou base64)',
|
|
640
|
+
placeholder: 'templateDoc ou /path/to/template.docx',
|
|
641
|
+
},
|
|
642
|
+
// Fill - Output
|
|
643
|
+
{
|
|
644
|
+
displayName: 'Output Property',
|
|
645
|
+
name: 'outputProperty',
|
|
646
|
+
type: 'string',
|
|
647
|
+
default: 'data',
|
|
648
|
+
displayOptions: { show: { operation: ['fill'] } },
|
|
649
|
+
description: 'Nom de la propriété binaire pour le document de sortie',
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
displayName: 'Use LLM for Mapping',
|
|
653
|
+
name: 'useLLM',
|
|
654
|
+
type: 'boolean',
|
|
655
|
+
default: false,
|
|
656
|
+
displayOptions: { show: { operation: ['fill'] } },
|
|
657
|
+
description: 'Utiliser le LLM connecté pour un mapping plus intelligent (plus lent)',
|
|
658
|
+
},
|
|
659
|
+
// Extract
|
|
660
|
+
{
|
|
661
|
+
displayName: 'Document Input Type',
|
|
662
|
+
name: 'extractInputType',
|
|
663
|
+
type: 'options',
|
|
664
|
+
displayOptions: { show: { operation: ['extract'] } },
|
|
665
|
+
options: [
|
|
666
|
+
{ name: 'Binary', value: 'binary' },
|
|
667
|
+
{ name: 'File Path', value: 'path' },
|
|
668
|
+
{ name: 'Auto-Detect', value: 'auto' },
|
|
669
|
+
],
|
|
670
|
+
default: 'auto',
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
displayName: 'Document',
|
|
674
|
+
name: 'extractDocument',
|
|
675
|
+
type: 'string',
|
|
676
|
+
default: '',
|
|
677
|
+
required: true,
|
|
678
|
+
displayOptions: { show: { operation: ['extract'] } },
|
|
679
|
+
description: 'Document à analyser',
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
async execute() {
|
|
685
|
+
var _a, _b, _c;
|
|
686
|
+
const items = this.getInputData();
|
|
687
|
+
const returnData = [];
|
|
688
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
689
|
+
// Récupérer le LLM si connecté
|
|
690
|
+
let llm = null;
|
|
691
|
+
try {
|
|
692
|
+
llm = await this.getInputConnectionData('ai_languageModel', 0);
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
// Pas de LLM connecté, on utilisera le mode sans LLM
|
|
696
|
+
}
|
|
697
|
+
for (let i = 0; i < items.length; i++) {
|
|
698
|
+
try {
|
|
699
|
+
if (operation === 'fill') {
|
|
700
|
+
const sourceInputType = this.getNodeParameter('sourceInputType', i);
|
|
701
|
+
const sourceValue = this.getNodeParameter('sourceDocument', i);
|
|
702
|
+
const templateInputType = this.getNodeParameter('templateInputType', i);
|
|
703
|
+
const templateValue = this.getNodeParameter('templateDocument', i);
|
|
704
|
+
const outputProperty = this.getNodeParameter('outputProperty', i);
|
|
705
|
+
const useLLM = this.getNodeParameter('useLLM', i);
|
|
706
|
+
// Charger les documents
|
|
707
|
+
const sourceBuffer = await getDocumentBuffer(this, i, sourceInputType, sourceValue, items, 'source');
|
|
708
|
+
const templateBuffer = await getDocumentBuffer(this, i, templateInputType, templateValue, items, 'template');
|
|
709
|
+
const sourceZip = new pizzip_1.default(sourceBuffer);
|
|
710
|
+
const templateZip = new pizzip_1.default(templateBuffer);
|
|
711
|
+
const sourceXml = ((_a = sourceZip.file('word/document.xml')) === null || _a === void 0 ? void 0 : _a.asText()) || '';
|
|
712
|
+
let templateXml = ((_b = templateZip.file('word/document.xml')) === null || _b === void 0 ? void 0 : _b.asText()) || '';
|
|
713
|
+
const sourceParagraphs = extractParagraphsWithPositions(sourceXml);
|
|
714
|
+
const templateParagraphs = extractParagraphsWithPositions(templateXml);
|
|
715
|
+
// Mode avec ou sans LLM
|
|
716
|
+
let sourceData;
|
|
717
|
+
let filledFields = [];
|
|
718
|
+
let modifiedCheckboxes = 0;
|
|
719
|
+
if (useLLM && llm) {
|
|
720
|
+
// Mode LLM: utiliser le LLM pour l'extraction et le mapping
|
|
721
|
+
const sourceStructured = docxToStructuredText(sourceParagraphs);
|
|
722
|
+
const templateStructured = docxToStructuredText(templateParagraphs);
|
|
723
|
+
// Extraction avec LLM
|
|
724
|
+
const extractionPrompt = EXTRACTION_PROMPT.replace('{document_text}', sourceStructured);
|
|
725
|
+
const extractionResponse = await llm.invoke(extractionPrompt);
|
|
726
|
+
const extractionText = typeof extractionResponse === 'string'
|
|
727
|
+
? extractionResponse
|
|
728
|
+
: extractionResponse.content || extractionResponse.text || JSON.stringify(extractionResponse);
|
|
729
|
+
try {
|
|
730
|
+
const jsonMatch = extractionText.match(/\{[\s\S]*\}/);
|
|
731
|
+
if (jsonMatch) {
|
|
732
|
+
sourceData = JSON.parse(jsonMatch[0]);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
throw new Error('No JSON in LLM response');
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// Fallback au mode sans LLM
|
|
740
|
+
sourceData = extractSourceData(sourceParagraphs);
|
|
741
|
+
}
|
|
742
|
+
// Mapping avec LLM
|
|
743
|
+
const fillPrompt = FILL_PROMPT
|
|
744
|
+
.replace('{source_data}', JSON.stringify(sourceData, null, 2))
|
|
745
|
+
.replace('{template_text}', templateStructured);
|
|
746
|
+
const fillResponse = await llm.invoke(fillPrompt);
|
|
747
|
+
const fillText = typeof fillResponse === 'string'
|
|
748
|
+
? fillResponse
|
|
749
|
+
: fillResponse.content || fillResponse.text || JSON.stringify(fillResponse);
|
|
750
|
+
// Parser les instructions et appliquer
|
|
751
|
+
// ... (logique similaire à avant)
|
|
752
|
+
// Pour simplifier, on utilise aussi la logique standard
|
|
753
|
+
const fillPositions = findFillPositions(templateParagraphs);
|
|
754
|
+
const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
|
|
755
|
+
templateXml = result.xml;
|
|
756
|
+
filledFields = result.filledFields;
|
|
757
|
+
modifiedCheckboxes = result.modifiedCheckboxes;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
// Mode sans LLM: extraction et mapping par patterns
|
|
761
|
+
sourceData = extractSourceData(sourceParagraphs);
|
|
762
|
+
const fillPositions = findFillPositions(templateParagraphs);
|
|
763
|
+
const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
|
|
764
|
+
templateXml = result.xml;
|
|
765
|
+
filledFields = result.filledFields;
|
|
766
|
+
modifiedCheckboxes = result.modifiedCheckboxes;
|
|
767
|
+
}
|
|
768
|
+
// Sauvegarder le document
|
|
769
|
+
templateZip.file('word/document.xml', templateXml);
|
|
770
|
+
const outputBuffer = templateZip.generate({
|
|
771
|
+
type: 'nodebuffer',
|
|
772
|
+
compression: 'DEFLATE',
|
|
773
|
+
});
|
|
774
|
+
// Générer le nom de fichier
|
|
775
|
+
const date = new Date().toISOString().split('T')[0];
|
|
776
|
+
const companyName = (sourceData.companyName || 'Document').replace(/[^a-zA-Z0-9]/g, '_');
|
|
777
|
+
const outputFilename = `${companyName}_${sourceData.documentType}_${date}.docx`;
|
|
778
|
+
const binaryData = await this.helpers.prepareBinaryData(outputBuffer, outputFilename, DOCX_MIME_TYPE);
|
|
779
|
+
returnData.push({
|
|
780
|
+
json: {
|
|
781
|
+
success: true,
|
|
782
|
+
filename: outputFilename,
|
|
783
|
+
documentType: sourceData.documentType,
|
|
784
|
+
companyName: sourceData.companyName,
|
|
785
|
+
filledFields,
|
|
786
|
+
modifiedCheckboxes,
|
|
787
|
+
extractedFieldsCount: sourceData.fields.length,
|
|
788
|
+
usedLLM: useLLM && !!llm,
|
|
789
|
+
message: `Rempli: ${filledFields.length} champs, ${modifiedCheckboxes} checkboxes`,
|
|
790
|
+
},
|
|
791
|
+
binary: {
|
|
792
|
+
[outputProperty]: binaryData,
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
else if (operation === 'extract') {
|
|
797
|
+
const inputType = this.getNodeParameter('extractInputType', i);
|
|
798
|
+
const inputValue = this.getNodeParameter('extractDocument', i);
|
|
799
|
+
const docBuffer = await getDocumentBuffer(this, i, inputType, inputValue, items, 'document');
|
|
800
|
+
const zip = new pizzip_1.default(docBuffer);
|
|
801
|
+
const xml = ((_c = zip.file('word/document.xml')) === null || _c === void 0 ? void 0 : _c.asText()) || '';
|
|
802
|
+
const paragraphs = extractParagraphsWithPositions(xml);
|
|
803
|
+
let extractedData;
|
|
804
|
+
if (llm) {
|
|
805
|
+
// Mode LLM
|
|
806
|
+
const structuredText = docxToStructuredText(paragraphs);
|
|
807
|
+
const prompt = EXTRACTION_PROMPT.replace('{document_text}', structuredText);
|
|
808
|
+
const response = await llm.invoke(prompt);
|
|
809
|
+
const responseText = typeof response === 'string'
|
|
810
|
+
? response
|
|
811
|
+
: response.content || response.text || JSON.stringify(response);
|
|
812
|
+
try {
|
|
813
|
+
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
|
814
|
+
if (jsonMatch) {
|
|
815
|
+
extractedData = JSON.parse(jsonMatch[0]);
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
throw new Error('No JSON');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
extractedData = extractSourceData(paragraphs);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
// Mode sans LLM
|
|
827
|
+
extractedData = extractSourceData(paragraphs);
|
|
828
|
+
}
|
|
829
|
+
returnData.push({
|
|
830
|
+
json: {
|
|
831
|
+
success: true,
|
|
832
|
+
documentType: extractedData.documentType,
|
|
833
|
+
companyName: extractedData.companyName,
|
|
834
|
+
fields: extractedData.fields,
|
|
835
|
+
checkboxes: extractedData.checkboxes,
|
|
836
|
+
fieldCount: extractedData.fields.length,
|
|
837
|
+
usedLLM: !!llm,
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
if (this.continueOnFail()) {
|
|
844
|
+
returnData.push({
|
|
845
|
+
json: {
|
|
846
|
+
success: false,
|
|
847
|
+
error: error.message,
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return [returnData];
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
exports.DocxFillerAI = DocxFillerAI;
|