data-primals-engine 1.4.3 → 1.5.1
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.
- package/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- package/test/import_export.integration.test.js +1 -1
|
@@ -12,6 +12,7 @@ import {maxAIReflectiveSteps} from "../../constants.js";
|
|
|
12
12
|
import {providers} from "./constants.js";
|
|
13
13
|
import {ChatDeepSeek} from "@langchain/deepseek";
|
|
14
14
|
import {ChatAnthropic} from "@langchain/anthropic";
|
|
15
|
+
import {Config} from "../../config.js";
|
|
15
16
|
|
|
16
17
|
let logger = null;
|
|
17
18
|
|
|
@@ -52,14 +53,40 @@ export const assistantGlobalLimiter = rateLimit({
|
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
async function searchModels(query, user) {
|
|
55
|
-
if (!query) return [];
|
|
56
|
+
if (!query) return { main: [], related: [] };
|
|
56
57
|
const searchRegex = new RegExp(query, 'i');
|
|
57
|
-
|
|
58
|
+
const mainModels = await modelsCollection.find({
|
|
58
59
|
$or: [{ _user: user.username }, { _user: { $exists: false } }],
|
|
59
60
|
$and: [{ $or: [{ name: { $regex: searchRegex } }, { description: { $regex: searchRegex } }] }]
|
|
60
61
|
}, {
|
|
61
62
|
projection: { name: 1, description: 1, fields: 1, _id: 0 }
|
|
62
63
|
}).limit(10).toArray();
|
|
64
|
+
|
|
65
|
+
if (mainModels.length === 0) {
|
|
66
|
+
return { main: [], related: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const relatedModelNames = new Set();
|
|
70
|
+
mainModels.forEach(model => {
|
|
71
|
+
model.fields.forEach(field => {
|
|
72
|
+
if (field.type === 'relation' && field.relation) {
|
|
73
|
+
relatedModelNames.add(field.relation);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const mainModelNames = new Set(mainModels.map(m => m.name));
|
|
79
|
+
const finalRelatedModelNames = [...relatedModelNames].filter(name => !mainModelNames.has(name));
|
|
80
|
+
|
|
81
|
+
let relatedModels = [];
|
|
82
|
+
if (finalRelatedModelNames.length > 0) {
|
|
83
|
+
relatedModels = await modelsCollection.find({
|
|
84
|
+
name: { $in: finalRelatedModelNames },
|
|
85
|
+
$or: [{ _user: user.username }, { _user: { $exists: false } }]
|
|
86
|
+
}, { projection: { name: 1, description: 1, fields: 1, _id: 0 } }).toArray();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { main: mainModels, related: relatedModels };
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
const createSystemPrompt = (modelDefs, lang) => {
|
|
@@ -86,7 +113,7 @@ Tu as accès aux outils et actions suivants.
|
|
|
86
113
|
|
|
87
114
|
OUTILS DE RAISONNEMENT INTERNE: (Utilisés pour collecter de l'information avant de décider de l'action finale)
|
|
88
115
|
1. **search_models**: Pour rechercher les modèles de données disponibles.
|
|
89
|
-
- Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }
|
|
116
|
+
- Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }. Il retourne aussi la structure des modèles directement liés.
|
|
90
117
|
====
|
|
91
118
|
|
|
92
119
|
ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat à l'utilisateur)
|
|
@@ -118,9 +145,24 @@ ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat
|
|
|
118
145
|
- \`yAxis\` (string): (Optionnel, sauf pour sum/avg/min/max) Le champ numérique à agréger du modèle.
|
|
119
146
|
- \`filter\` (object): (Optionnel, filtre de la recherche, même écriture stricte que pour les filtres de recherche (voir plus bas pour les exemples)
|
|
120
147
|
|
|
148
|
+
7. **generateHtmlView**: Pour créer une vue personnalisée en utilisant un template HTML.
|
|
149
|
+
- Utilisation: { "action": "generateHtmlView", "params": { ...config } }
|
|
150
|
+
- Le paramètre \`config\` doit contenir :
|
|
151
|
+
- \`title\` (string): Un titre pour la vue.
|
|
152
|
+
- \`model\` (string): Le nom du modèle de données à utiliser.
|
|
153
|
+
- \`template\` (string): Un template au format Handlebars.js. **RÈGLE CRITIQUE : N'utilise QUE les noms de champs (\`fieldName\`) exacts fournis par l'outil \`search_models\` pour le modèle principal ET pour ses modèles liés. N'invente JAMAIS de champs.**
|
|
154
|
+
Pour une liste, tu DOIS utiliser une boucle \`{{#each data}}...{{/each}}\`.
|
|
155
|
+
À l'intérieur d'une boucle, accède aux champs avec \`{{this.fieldName}}\`.
|
|
156
|
+
Pour les champs de type \`string_t\` ou \`richtext_t\`, accède à la traduction avec \`{{this.fieldName.value}}\`.
|
|
157
|
+
Les champs de type 'relation' sont automatiquement peuplés (hydratés), tu peux donc accéder à leurs propriétés directement (ex: \`{{this.relationField.name}}\` ou \`{{this.relationField.name.value}}\` si le champ 'name' de la relation est un 'string_t').
|
|
158
|
+
- \`css\` (string): (Optionnel) Du CSS riche et créatif pour styliser le template. N'hésite pas à utiliser des dégradés, des ombres, des animations et des polices de caractères pour un rendu professionnel et attrayant. **Règle absolue : tu dois préfixer TOUS tes sélecteurs avec \`#{{containerId}}\` pour isoler les styles.**
|
|
159
|
+
- \`filter\` (object): (Optionnel) Un filtre pour sélectionner les documents à afficher.
|
|
160
|
+
- \`limit\` (number): (Optionnel, défaut 10) Le nombre maximum de documents à récupérer.
|
|
161
|
+
|
|
121
162
|
Voici le mémo pour assigner des valeurs aux champs des modèles,avec ces types de données :
|
|
122
163
|
utilise une chaine de caractère convertible en ObjectId (mongodb) lorsque le nom du champ est _id
|
|
123
|
-
utilise une chaine de caracteres lorsque le type de champ est : string
|
|
164
|
+
utilise une chaine de caracteres lorsque le type de champ est : string , password, url, phone, email, richtext
|
|
165
|
+
utilise un objet { key: "trKey", value: "Translation"} lorsque le champ est string_t
|
|
124
166
|
utilise un filtre en retour si le type de champ est code et language='json' et conditionBuilder=true
|
|
125
167
|
utilise une chaine si c'est un type de champ code par défaut.
|
|
126
168
|
utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
|
|
@@ -134,13 +176,11 @@ utilise un tableau d'_ids pour remplir les champs relation multiple=true
|
|
|
134
176
|
utilise la valeur en héxadecimal, ex: '#FF0000' pour les champs de type : color
|
|
135
177
|
utilise les valeurs de cron standard '* * * * * *' pour : cronSchedule
|
|
136
178
|
|
|
137
|
-
PROCESSUS DE RAISONNEMENT:
|
|
138
|
-
a- L'utilisateur pose une question
|
|
139
|
-
b-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Si tu n'as aucune commande à exécuter directement, réponds simplement à l'utilisateur avec "displayMessage".
|
|
143
|
-
|
|
179
|
+
PROCESSUS DE RAISONNEMENT STRICT:
|
|
180
|
+
a- L'utilisateur pose une question.
|
|
181
|
+
b- **Étape 1 (Obligatoire):** Appelle l'outil \`search_models\` pour obtenir la structure EXACTE du modèle de données. C'est ta seule source de vérité pour les noms de champs.
|
|
182
|
+
c- **Étape 2 (Obligatoire):** Analyse la réponse de \`search_models\` que le système t'a fournie.
|
|
183
|
+
d- **Étape 3 (Commande Finale):** Construis ta commande finale (\`search\`, \`generateHtmlView\`, etc.). **Règle absolue :** Pour les filtres et les templates, tu ne dois utiliser QUE les noms de champs (\`name\`) et les types (\`type\`) que tu as lus dans la réponse de \`search_models\` à l'étape c. N'invente RIEN.
|
|
144
184
|
CONTEXTE ACTUEL:
|
|
145
185
|
- Date du jour de la conversation : ${dt}
|
|
146
186
|
- La langue ISO à utiliser dans la conversation : ${lang}
|
|
@@ -182,6 +222,67 @@ COMMANDE FINALE :
|
|
|
182
222
|
}
|
|
183
223
|
}
|
|
184
224
|
|
|
225
|
+
Question: Crée un tableau de bord des rôles et permissions avec un design sophistiqué.
|
|
226
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "role" } }
|
|
227
|
+
COMMANDE FINALE :
|
|
228
|
+
{
|
|
229
|
+
"action": "generateHtmlView",
|
|
230
|
+
"params": {
|
|
231
|
+
"title": "Matrice des Rôles et Permissions",
|
|
232
|
+
"model": "role",
|
|
233
|
+
"template": "<div class='roles-container'>{{#each data}}<div class='role-card'><div class='role-header'><span class='role-icon'>🛡️</span><h3>{{this.name.value}}</h3></div><ul class='permissions-list'>{{#each this.permissions}}<li data-tooltip-html='{{this.description}}'><span class='permission-name'>{{this.name.value}}</span></li>{{/each}}</ul></div>{{/each}}</div>",
|
|
234
|
+
"css": "#{{containerId}} .roles-container{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:25px;padding:25px;font-family:'Poppins',sans-serif;background:#111827}#{{containerId}} .role-card{background:rgba(31,41,55,.5);border-radius:16px;padding:20px;position:relative;overflow:hidden;border:1px solid transparent;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);transition:transform .3s ease,box-shadow .3s ease}#{{containerId}} .role-card::before{content:'';position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;margin:-1px;border-radius:inherit;background:conic-gradient(from 180deg at 50% 50%,#2a8af6 0deg,#a855f7 180deg,#f59e0b 360deg);animation:rotate-gradient 5s linear infinite}@keyframes rotate-gradient{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}#{{containerId}} .role-card:hover{transform:translateY(-8px);box-shadow:0 20px 30px rgba(0,0,0,.2)}#{{containerId}} .role-header{display:flex;align-items:center;gap:12px;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,.1)}#{{containerId}} .role-header .role-icon{font-size:1.8em}#{{containerId}} .role-header h3{margin:0;font-size:1.4em;font-weight:600;color:#f9fafb}#{{containerId}} .permissions-list{list-style:none;padding:0;margin:0;max-height:200px;overflow-y:auto}#{{containerId}} .permissions-list::-webkit-scrollbar{width:6px}#{{containerId}} .permissions-list::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:3px}#{{containerId}} .permissions-list::-webkit-scrollbar-thumb{background:#4f46e5;border-radius:3px}#{{containerId}} .permissions-list::-webkit-scrollbar-thumb:hover{background:#6366f1}#{{containerId}} .permissions-list li{padding:8px 12px;margin-bottom:6px;background:rgba(255,255,255,.05);border-radius:8px;color:#d1d5db;cursor:help;transition:background-color .2s ease;font-size:.95em}#{{containerId}} .permissions-list li:hover{background:rgba(79,70,229,.5);color:#fff}",
|
|
235
|
+
"filter": {},
|
|
236
|
+
"limit": 10
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Question: Affiche une carte de visite stylisée pour le premier contact.
|
|
241
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "contact" } }
|
|
242
|
+
COMMANDE FINALE :
|
|
243
|
+
{
|
|
244
|
+
"action": "generateHtmlView",
|
|
245
|
+
"params": {
|
|
246
|
+
"title": "Carte de Visite",
|
|
247
|
+
"model": "contact",
|
|
248
|
+
"template": "<div class='card-container'><h3>{{this.firstName}} {{this.lastName}}</h3><p>{{this.email}}</p></div>",
|
|
249
|
+
"css": "#{{containerId}} .card-container { border: 1px solid #ccc; border-radius: 8px; padding: 16px; background-color: #f9f9f9; } #{{containerId}} h3 { margin-top: 0; color: #333; }",
|
|
250
|
+
"filter": {},
|
|
251
|
+
"limit": 1
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Question: Montre-moi un catalogue des produits.
|
|
256
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "product" } }
|
|
257
|
+
COMMANDE FINALE :
|
|
258
|
+
{
|
|
259
|
+
"action": "generateHtmlView",
|
|
260
|
+
"params": {
|
|
261
|
+
"title": "Catalogue Produits",
|
|
262
|
+
"model": "product",
|
|
263
|
+
"template": "<div class='product-grid'>{{#each data}}<div class='product-card'><div class='product-image-container'><img src='{{this.image.0.url}}' alt='{{this.name.value}}' class='product-image'><span class='product-brand'>{{this.brand.name}}</span></div><div class='product-info'><h3 class='product-name'>{{this.name.value}}</h3><p class='product-category'>{{this.category.name.value}}</p><div class='product-footer'><span class='product-price'>{{this.price}} {{this.currency.symbol}}</span><button class='add-to-cart-btn'>Ajouter au panier</button></div></div></div>{{/each}}</div>",
|
|
264
|
+
"css": "#{{containerId}} .product-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:2rem;padding:2rem;font-family:'Lato',sans-serif;background-color:#f8f9fa}#{{containerId}} .product-card{background-color:#fff;border-radius:12px;box-shadow:0 4px 6px rgba(0,0,0,.05),0 1px 3px rgba(0,0,0,.05);overflow:hidden;transition:transform .3s ease,box-shadow .3s ease;display:flex;flex-direction:column}#{{containerId}} .product-card:hover{transform:translateY(-5px);box-shadow:0 12px 20px rgba(0,0,0,.08),0 3px 8px rgba(0,0,0,.06)}#{{containerId}} .product-image-container{position:relative;width:100%;padding-top:100%}#{{containerId}} .product-image{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover}#{{containerId}} .product-brand{position:absolute;top:10px;right:10px;background-color:rgba(0,0,0,.6);color:#fff;padding:4px 8px;border-radius:6px;font-size:.8em;font-weight:700}#{{containerId}} .product-info{padding:15px;display:flex;flex-direction:column;flex-grow:1}#{{containerId}} .product-name{font-size:1.1em;font-weight:700;color:#343a40;margin:0 0 5px 0;line-height:1.3}#{{containerId}} .product-category{font-size:.85em;color:#6c757d;margin:0 0 15px 0;flex-grow:1}#{{containerId}} .product-footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #e9ecef;padding-top:10px;margin-top:auto}#{{containerId}} .product-price{font-size:1.2em;font-weight:700;color:#007bff}#{{containerId}} .add-to-cart-btn{background-color:#007bff;color:#fff;border:none;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;transition:background-color .2s ease}#{{containerId}} .add-to-cart-btn:hover{background-color:#0056b3}",
|
|
265
|
+
"filter": {},
|
|
266
|
+
"limit": 12
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Question: Affiche-moi les dernières requêtes API avec un design futuriste.
|
|
271
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "request" } }
|
|
272
|
+
COMMANDE FINALE :
|
|
273
|
+
{
|
|
274
|
+
"action": "generateHtmlView",
|
|
275
|
+
"params": {
|
|
276
|
+
"title": "Journal des Requêtes API",
|
|
277
|
+
"model": "request",
|
|
278
|
+
"template": "<div class=\"requests-grid\">{{#each data}}<div class=\"request-card status-{{this.status}}\"><div class=\"card-header\"><span class=\"method-badge method-{{this.method}}\">{{this.method}}</span><span class=\"status-code\" data-tooltip-html=\"Code de statut HTTP\">{{this.status}}</span></div><div class=\"card-body\"><p class=\"url\" data-tooltip-html=\"URL de la requête\">{{this.url}}</p></div><div class=\"card-footer\"><span class=\"latency\" data-tooltip-html=\"Latence de la réponse\">⏱️ {{this.latencyMs}} ms</span><span class=\"timestamp\" data-tooltip-html=\"Date et heure\">{{this.timestamp}}</span></div><div class=\"glow-effect\"></div></div>{{/each}}</div>",
|
|
279
|
+
"css": "#{{containerId}} .requests-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;font-family:'Orbitron',sans-serif;padding:10px}#{{containerId}} .request-card{background:rgba(10,25,47,.8);border:1px solid #00aaff;border-radius:12px;padding:15px;position:relative;overflow:hidden;transition:transform .3s ease,box-shadow .3s ease;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)}#{{containerId}} .request-card:hover{transform:translateY(-5px);box-shadow:0 10px 20px rgba(0,170,255,.3)}#{{containerId}} .glow-effect{position:absolute;top:0;left:0;width:100%;height:100%;background:radial-gradient(circle at 50% 0,rgba(0,170,255,.2),transparent 70%);animation:pulse 4s infinite ease-in-out;pointer-events:none}@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}#{{containerId}} .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;border-bottom:1px solid rgba(0,170,255,.2);padding-bottom:8px}#{{containerId}} .method-badge{padding:4px 8px;border-radius:5px;font-weight:700;font-size:.9em;color:#fff;text-shadow:0 0 5px currentColor}#{{containerId}} .method-GET{background-color:#00aaff}#{{containerId}} .method-POST{background-color:#4caf50}#{{containerId}} .method-PUT{background-color:#ff9800}#{{containerId}} .method-DELETE{background-color:#f44336}#{{containerId}} .method-PATCH{background-color:#9c27b0}#{{containerId}} .status-code{font-size:1.2em;font-weight:700;color:#fff}#{{containerId}} .status-200 .status-code{color:#4caf50;text-shadow:0 0 8px #4caf50}#{{containerId}} .status-404 .status-code{color:#f44336;text-shadow:0 0 8px #f44336}#{{containerId}} .status-500 .status-code{color:#ff9800;text-shadow:0 0 8px #ff9800}#{{containerId}} .card-body .url{color:#e0e0e0;font-size:.95em;word-break:break-all;margin:0}#{{containerId}} .card-footer{display:flex;justify-content:space-between;align-items:center;margin-top:15px;font-size:.8em;color:#88a1b9}#{{containerId}} .latency,#{{containerId}} .timestamp{display:flex;align-items:center;gap:5px}",
|
|
280
|
+
"filter": {},
|
|
281
|
+
"sort": { "_id": -1 },
|
|
282
|
+
"limit": 12
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
185
286
|
Absolute rules:
|
|
186
287
|
====
|
|
187
288
|
NEVER use the non-aggregated syntax of the MongoDB operators :
|
|
@@ -274,11 +375,17 @@ async function executeTool(action, params, user, allModels) {
|
|
|
274
375
|
return resultString;
|
|
275
376
|
}
|
|
276
377
|
case 'search_models': {
|
|
277
|
-
const foundModels = await searchModels(params.query, user);
|
|
378
|
+
const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
|
|
278
379
|
|
|
279
380
|
if (foundModels.length > 0) {
|
|
280
|
-
|
|
381
|
+
let responseText = "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
|
|
281
382
|
foundModels.map(m => `\n- Modèle "${m.name}": ${m.description || 'Pas de description.'}\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
|
|
383
|
+
|
|
384
|
+
if (relatedModels.length > 0) {
|
|
385
|
+
responseText += "\n\nPour votre information, voici la structure des modèles liés que vous pouvez utiliser dans les templates :";
|
|
386
|
+
responseText += relatedModels.map(m => `\n- Modèle lié "${m.name}":\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
|
|
387
|
+
}
|
|
388
|
+
return responseText;
|
|
282
389
|
} else {
|
|
283
390
|
return "Je n'ai trouvé aucun modèle correspondant à votre recherche.";
|
|
284
391
|
}
|
|
@@ -356,7 +463,8 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
356
463
|
|
|
357
464
|
|
|
358
465
|
// --- BOUCLE DE RAISONNEMENT ET D'EXÉCUTION D'OUTILS ---
|
|
359
|
-
|
|
466
|
+
const m = Config.Get('maxAIReflectiveSteps', maxAIReflectiveSteps);
|
|
467
|
+
for (let i = 0; i < m; i++) {
|
|
360
468
|
logger.debug(`[Assistant] Tour de boucle ${i + 1}. Invocation de l'IA...`);
|
|
361
469
|
|
|
362
470
|
const response = await llm.invoke(conversationHistory);
|
|
@@ -365,18 +473,25 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
365
473
|
// Parsing JSON robuste
|
|
366
474
|
let parsedResponse;
|
|
367
475
|
try {
|
|
476
|
+
// Tente d'extraire le JSON de la réponse, même s'il est entouré de texte.
|
|
477
|
+
const jsonRegex = /\{[\s\S]*\}/s; // 's' flag pour que '.' matche les nouvelles lignes
|
|
478
|
+
const match = llmOutput.match(jsonRegex);
|
|
368
479
|
|
|
369
|
-
|
|
480
|
+
if (match && match[0]) {
|
|
481
|
+
// Si un JSON est trouvé, on tente de le parser
|
|
482
|
+
parsedResponse = JSON.parse(match[0]);
|
|
483
|
+
} else {
|
|
484
|
+
// Aucun JSON trouvé, c'est probablement une réponse textuelle simple.
|
|
485
|
+
return { success: true, displayMessage: llmOutput };
|
|
486
|
+
}
|
|
370
487
|
|
|
371
488
|
if (!parsedResponse.action || !parsedResponse.params) {
|
|
372
489
|
throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
|
|
373
490
|
}
|
|
374
491
|
} catch (parseError) {
|
|
375
492
|
logger.error(`[Assistant] Erreur de parsing de la réponse de l'IA: ${parseError.message}. Réponse brute: "${llmOutput}"`);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
displayMessage: llmOutput || i18n.t('assistant.invalidResponse', "Désolé, je n'ai pas pu formuler une réponse correcte. Veuillez réessayer.")
|
|
379
|
-
};
|
|
493
|
+
// Si le parsing échoue, on renvoie le message brut de l'IA, qui est peut-être une réponse textuelle valide.
|
|
494
|
+
return { success: true, displayMessage: llmOutput };
|
|
380
495
|
}
|
|
381
496
|
|
|
382
497
|
logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
|
|
@@ -396,6 +511,23 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
396
511
|
return { success: true, chartConfig: params };
|
|
397
512
|
}
|
|
398
513
|
|
|
514
|
+
// Action de génération de vue HTML
|
|
515
|
+
if (action === 'generateHtmlView') {
|
|
516
|
+
const viewData = await searchData({
|
|
517
|
+
model: params.model,
|
|
518
|
+
filter: params.filter,
|
|
519
|
+
limit: params.limit || 10,
|
|
520
|
+
depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
|
|
521
|
+
}, user);
|
|
522
|
+
|
|
523
|
+
if (viewData.data.length === 0) {
|
|
524
|
+
return { success: true, displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.") };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// On retourne la configuration de la vue ET les données au client.
|
|
528
|
+
return { success: true, htmlViewConfig: { ...params, data: viewData.data } };
|
|
529
|
+
}
|
|
530
|
+
|
|
399
531
|
// NOUVEAU: Action de recherche à afficher, gérée par le front-end
|
|
400
532
|
if (action === 'search') {
|
|
401
533
|
const searchResult = await searchData({
|
package/src/modules/bucket.js
CHANGED
|
@@ -6,13 +6,14 @@ import {decryptValue, encryptValue} from "../data.js";
|
|
|
6
6
|
import {loadFromDump, validateRestoreRequest} from "./data/index.js";
|
|
7
7
|
import {Logger} from "../gameObject.js";
|
|
8
8
|
import {middlewareAuthenticator, userInitiator} from "./user.js";
|
|
9
|
-
import {awsDefaultConfig, maxBytesPerSecondThrottleData} from "../constants.js";
|
|
9
|
+
import {awsDefaultConfig, getHost, maxBytesPerSecondThrottleData} from "../constants.js";
|
|
10
10
|
import crypto from "node:crypto";
|
|
11
11
|
import i18n from "../../src/i18n.js";
|
|
12
12
|
import {sendEmail} from "../email.js";
|
|
13
13
|
import {throttleMiddleware} from "../middlewares/throttle.js";
|
|
14
14
|
import {getCollectionForUser} from "./mongodb.js";
|
|
15
15
|
import {safeAssignObject} from "../core.js";
|
|
16
|
+
import {Config} from "../config.js";
|
|
16
17
|
|
|
17
18
|
const restoreRequests = {};
|
|
18
19
|
|
|
@@ -37,6 +38,7 @@ export const requestRestore = async (user, lang) => {
|
|
|
37
38
|
content: i18n.t('email.backup.restoreRequest.content', {
|
|
38
39
|
user: user?.username,
|
|
39
40
|
fullToken: fullRestoreToken,
|
|
41
|
+
host: getHost(),
|
|
40
42
|
modelsToken: modelsRestoreToken
|
|
41
43
|
})
|
|
42
44
|
});
|
|
@@ -47,15 +49,6 @@ export const requestRestore = async (user, lang) => {
|
|
|
47
49
|
}
|
|
48
50
|
};
|
|
49
51
|
|
|
50
|
-
const getDefaultS3Config = () => {
|
|
51
|
-
return {
|
|
52
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
53
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // Utiliser la clé déchiffrée
|
|
54
|
-
region: process.env.AWS_REGION || awsDefaultConfig.region,
|
|
55
|
-
bucketName: process.env.AWS_BUCKET || awsDefaultConfig.bucketName
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
52
|
/**
|
|
60
53
|
* Récupère la configuration S3 depuis les variables d'environnement de l'utilisateur en base de données.
|
|
61
54
|
* @param {object} user - L'objet utilisateur.
|
|
@@ -98,12 +91,16 @@ async function _getUserS3ConfigFromDb(user) {
|
|
|
98
91
|
* @returns {Promise<object>} - L'objet de configuration S3 final.
|
|
99
92
|
*/
|
|
100
93
|
export async function getUserS3Config(user) {
|
|
94
|
+
|
|
95
|
+
const adc = awsDefaultConfig;
|
|
96
|
+
const dc = Config.Get('awsDefaultConfig', {});
|
|
97
|
+
|
|
101
98
|
// 1. Récupérer la configuration globale par défaut
|
|
102
99
|
const defaultConfig = {
|
|
103
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
104
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
105
|
-
region: process.env.AWS_REGION ||
|
|
106
|
-
bucketName: process.env.AWS_BUCKET_NAME ||
|
|
100
|
+
accessKeyId: dc.accessKeyId || process.env.AWS_ACCESS_KEY_ID,
|
|
101
|
+
secretAccessKey: dc.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY,
|
|
102
|
+
region: dc.region || process.env.AWS_REGION || adc.region,
|
|
103
|
+
bucketName: dc.bucketName || process.env.AWS_BUCKET_NAME || adc.bucketName
|
|
107
104
|
};
|
|
108
105
|
|
|
109
106
|
// 2. Récupérer la configuration spécifique de l'utilisateur
|
|
@@ -243,13 +240,14 @@ export const getS3Stream = (s3Config, s3Key) => {
|
|
|
243
240
|
return s3.getObject(params).createReadStream();
|
|
244
241
|
};
|
|
245
242
|
|
|
246
|
-
const throttle = throttleMiddleware(maxBytesPerSecondThrottleData);
|
|
247
|
-
|
|
248
243
|
let engine, logger;
|
|
249
244
|
export async function onInit(defaultEngine) {
|
|
250
245
|
engine = defaultEngine;
|
|
251
246
|
logger = engine.getComponent(Logger);
|
|
252
247
|
|
|
248
|
+
const m = Config.Get('maxBytesPerSecondThrottleData', maxBytesPerSecondThrottleData);
|
|
249
|
+
const throttle = throttleMiddleware(m);
|
|
250
|
+
|
|
253
251
|
const userMiddlewares = await engine.userProvider.getMiddlewares();
|
|
254
252
|
engine.post('/api/backup/request-restore', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares], async (req, res) => {
|
|
255
253
|
const user = req.me; // Assuming you have user authentication middleware
|
|
@@ -153,7 +153,8 @@ export const loadFromDump = async (user, options = {}) => {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// --- EXÉCUTION DE MONGORESTORE ---
|
|
156
|
-
const
|
|
156
|
+
const d = Config.Get('dbName', dbName);
|
|
157
|
+
const restoreSourceDir = path.join(tmpRestoreDir, d);
|
|
157
158
|
if (!fs.existsSync(restoreSourceDir)) {
|
|
158
159
|
throw new Error(`Restore source directory (${restoreSourceDir}) not found.`);
|
|
159
160
|
}
|
|
@@ -161,15 +162,15 @@ export const loadFromDump = async (user, options = {}) => {
|
|
|
161
162
|
let command;
|
|
162
163
|
const args = [
|
|
163
164
|
'--uri', dbUrl,
|
|
164
|
-
'--db',
|
|
165
|
+
'--db', d
|
|
165
166
|
];
|
|
166
167
|
|
|
167
168
|
if (modelsOnly) {
|
|
168
|
-
args.push('--nsInclude', `${
|
|
169
|
+
args.push('--nsInclude', `${d}.models`);
|
|
169
170
|
} else {
|
|
170
171
|
// mongorestore accepte plusieurs fois l'option --nsInclude
|
|
171
|
-
args.push('--nsInclude', `${
|
|
172
|
-
args.push('--nsInclude', `${
|
|
172
|
+
args.push('--nsInclude', `${d}.datas`);
|
|
173
|
+
args.push('--nsInclude', `${d}.models`);
|
|
173
174
|
}
|
|
174
175
|
// Le répertoire source est le dernier argument
|
|
175
176
|
args.push(restoreSourceDir);
|
|
@@ -242,6 +243,8 @@ export const dumpUserData = async (user) => {
|
|
|
242
243
|
const backupFrequency = await engine.userProvider.getBackupFrequency(user);
|
|
243
244
|
logger.info(`Fréquence de sauvegarde : ${backupFrequency}.`);
|
|
244
245
|
|
|
246
|
+
|
|
247
|
+
const d = Config.Get('dbName', dbName);
|
|
245
248
|
const collections = await MongoDatabase.listCollections().toArray();
|
|
246
249
|
for (const collection of collections) {
|
|
247
250
|
const collsToBackup = [await getUserCollectionName(user), 'models'];
|
|
@@ -249,7 +252,7 @@ export const dumpUserData = async (user) => {
|
|
|
249
252
|
const query = {_user: user.username};
|
|
250
253
|
const args = [
|
|
251
254
|
'--uri', dbUrl,
|
|
252
|
-
'--db',
|
|
255
|
+
'--db', d,
|
|
253
256
|
'--out', localTempDumpDir,
|
|
254
257
|
'--collection', collection.name,
|
|
255
258
|
'--query', JSON.stringify(query)
|
|
@@ -258,9 +261,9 @@ export const dumpUserData = async (user) => {
|
|
|
258
261
|
await execFileAsync('mongodump', args);
|
|
259
262
|
}
|
|
260
263
|
}
|
|
261
|
-
const dumpSourceDir = path.join(localTempDumpDir,
|
|
264
|
+
const dumpSourceDir = path.join(localTempDumpDir, d);
|
|
262
265
|
if (fs.existsSync(dumpSourceDir)) {
|
|
263
|
-
await tar.create({gzip: true, file: localArchivePath, C: localTempDumpDir}, [
|
|
266
|
+
await tar.create({gzip: true, file: localArchivePath, C: localTempDumpDir}, [d]);
|
|
264
267
|
logger.info(`Archive de sauvegarde locale créée : ${localArchivePath}`);
|
|
265
268
|
} else {
|
|
266
269
|
logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a créée.`);
|
|
@@ -1,93 +1,119 @@
|
|
|
1
|
-
import NodeCache from "node-cache";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {Worker} from 'worker_threads';
|
|
4
|
-
import {fileURLToPath} from "node:url";
|
|
5
|
-
export const modelsCache = new NodeCache( { stdTTL: 100, checkperiod: 120 } );
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
import NodeCache from "node-cache";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {Worker} from 'worker_threads';
|
|
4
|
+
import {fileURLToPath} from "node:url";
|
|
5
|
+
export const modelsCache = new NodeCache( { stdTTL: 100, checkperiod: 120 } );
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper to escape characters for regex.
|
|
12
|
+
* @param {string} str The string to escape.
|
|
13
|
+
* @returns {string} The escaped string.
|
|
14
|
+
*/
|
|
15
|
+
function escapeRegex(str) {
|
|
16
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generates a regular expression from a mask pattern and replacement rules.
|
|
21
|
+
* @param {string} mask - The mask pattern, e.g., '+0 (___) ___-__-__'.
|
|
22
|
+
* @param {object} replacement - An object mapping placeholder characters to regex patterns, e.g., { "_": "\\d" }.
|
|
23
|
+
* @returns {string|null} - A regular expression string or null if inputs are invalid.
|
|
24
|
+
*/
|
|
25
|
+
export function generateRegexFromMask(mask, replacement) {
|
|
26
|
+
if (!mask || !replacement || typeof replacement !== 'object') return null;
|
|
27
|
+
|
|
28
|
+
let regex = '^';
|
|
29
|
+
for (const char of mask) {
|
|
30
|
+
regex += replacement[char] ? `(${replacement[char]})` : escapeRegex(char);
|
|
31
|
+
}
|
|
32
|
+
regex += '$';
|
|
33
|
+
return regex;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const mongoDBWhitelist = [
|
|
37
|
+
"$$NOW", "$in", "$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin", "$type", "$size",
|
|
38
|
+
"$and", "$not", "$nor", "$or", "$regexMatch", "$find", "$elemMatch", "$filter", "$toString", "$toObjectId",
|
|
39
|
+
"$concat",
|
|
40
|
+
'$add', '$subtract', '$multiply', '$divide', '$mod', '$pow', "$sqrt",
|
|
41
|
+
"$rand",
|
|
42
|
+
"$abs", '$sin', '$cos', '$tan', '$asin', '$acos', '$atan',
|
|
43
|
+
"$toDate", "$toBool", "$toString", "$toInt", "$toDouble",
|
|
44
|
+
"$dateDiff", "$dateSubtract", "$dateAdd", "$dateToString",
|
|
45
|
+
'$year', '$month', '$week', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$hour', '$minute', '$second', '$millisecond',
|
|
46
|
+
'$geoNear', '$regex', '$text', '$nearSphere','$geoWithin','$geoIntersects'
|
|
47
|
+
];
|
|
48
|
+
export let importJobs = {};
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Exécute une tâche d'import/export (parsing, stringify) dans un worker thread.
|
|
53
|
+
* @param {('parse-json'|'parse-csv'|'stringify-json')} action - L'action à effectuer.
|
|
54
|
+
* @param {object} payload - Les données nécessaires pour l'action.
|
|
55
|
+
* @returns {Promise<any>} - Une promesse qui se résout avec les données traitées.
|
|
56
|
+
*/
|
|
57
|
+
export function runImportExportWorker(action, payload) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const workerPath = path.resolve(process.cwd(), './src/workers/import-export-worker.js');
|
|
60
|
+
const worker = new Worker(workerPath);
|
|
61
|
+
|
|
62
|
+
worker.postMessage({ action, payload });
|
|
63
|
+
|
|
64
|
+
worker.on('message', (result) => {
|
|
65
|
+
if (result.success) {
|
|
66
|
+
resolve(result.data);
|
|
67
|
+
} else {
|
|
68
|
+
// Correction : On s'assure de toujours passer une chaîne de caractères à new Error()
|
|
69
|
+
const errorMessage = result.error || `Import/Export Worker failed with an unknown error. Action: ${action}.`;
|
|
70
|
+
reject(new Error(errorMessage));
|
|
71
|
+
}
|
|
72
|
+
worker.terminate();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
worker.on('error', (err) => {
|
|
76
|
+
reject(err);
|
|
77
|
+
worker.terminate();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
worker.on('exit', (code) => {
|
|
81
|
+
if (code !== 0) {
|
|
82
|
+
reject(new Error(`Import/Export Worker stopped with exit code ${code}`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Exécute une tâche de cryptographie dans un worker thread.
|
|
88
|
+
* @param {('encrypt'|'decrypt'|'hash')} action - L'action à effectuer.
|
|
89
|
+
* @param {object} payload - Les données nécessaires pour l'action.
|
|
90
|
+
* @returns {Promise<any>} - Une promesse qui se résout avec le résultat (si pertinent).
|
|
91
|
+
*/
|
|
92
|
+
export function runCryptoWorkerTask(action, payload) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const workerPath = path.resolve(__dirname, '../../workers/crypto-worker.js');
|
|
95
|
+
const worker = new Worker(workerPath);
|
|
96
|
+
|
|
97
|
+
worker.postMessage({ action, payload });
|
|
98
|
+
|
|
99
|
+
worker.on('message', (result) => {
|
|
100
|
+
if (result.success) {
|
|
101
|
+
resolve(result.data); // Résout avec les données (ex: le hash) ou undefined si pas de retour
|
|
102
|
+
} else {
|
|
103
|
+
reject(new Error(result.error));
|
|
104
|
+
}
|
|
105
|
+
worker.terminate();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
worker.on('error', (err) => {
|
|
109
|
+
reject(err);
|
|
110
|
+
worker.terminate();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
worker.on('exit', (code) => {
|
|
114
|
+
if (code !== 0) {
|
|
115
|
+
reject(new Error(`Crypto Worker stopped with exit code ${code}`));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
93
119
|
}
|