data-primals-engine 1.5.0 → 1.5.2
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 +37 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +2 -6
- package/client/src/App.scss +13 -1
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +27 -10
- 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 +654 -569
- package/client/src/DataEditor.jsx +10 -3
- package/client/src/DataLayout.jsx +807 -755
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +39 -75
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +2057 -1825
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +69 -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/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +5 -4
- package/client/src/ModelCreatorField.jsx +51 -4
- package/client/src/ModelList.jsx +280 -236
- 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/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +263 -212
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +24 -24
- package/package.json +7 -6
- package/src/constants.js +1 -1
- package/src/core.js +8 -1
- package/src/defaultModels.js +1596 -1544
- package/src/engine.js +85 -43
- package/src/events.js +137 -113
- package/src/i18n.js +710 -10
- package/src/index.js +3 -0
- package/src/modules/assistant/assistant.js +253 -134
- package/src/modules/assistant/constants.js +2 -1
- package/src/modules/bucket.js +2 -1
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +555 -492
- package/src/modules/data/data.js +3 -53
- package/src/modules/data/data.operations.js +3381 -3231
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1821
- package/src/modules/data/data.validation.js +81 -2
- package/src/modules/file.js +247 -238
- package/src/modules/user.js +1 -0
- package/src/modules/workflow.js +2 -2
- package/src/openai.jobs.js +3 -2
- package/src/packs.js +5482 -5478
- package/src/sso.js +2 -2
- package/src/workers/import-export-worker.js +1 -1
- package/test/data.history.integration.test.js +264 -192
- package/test/data.integration.test.js +149 -3
package/src/index.js
CHANGED
|
@@ -5,6 +5,9 @@ export { Engine } from './engine.js';
|
|
|
5
5
|
export { GameObject, Logger, BenchmarkTool } from './gameObject.js';
|
|
6
6
|
export { Config } from './config.js';
|
|
7
7
|
export { event_on, event_trigger, event_off } from './core.js';
|
|
8
|
+
export { Event } from './events.js';
|
|
9
|
+
|
|
10
|
+
export { sendSseToUser } from './modules/data/data.routes.js';
|
|
8
11
|
|
|
9
12
|
export { UserProvider } from './providers.js';
|
|
10
13
|
|
|
@@ -1,44 +1,47 @@
|
|
|
1
1
|
import { getCollectionForUser, modelsCollection } from "../mongodb.js";
|
|
2
2
|
import { Logger } from "../../gameObject.js";
|
|
3
|
-
import { ChatOpenAI } from "@langchain/openai";
|
|
4
|
-
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
5
3
|
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
6
4
|
import {searchData, patchData, deleteData, insertData} from "../data/index.js";
|
|
7
|
-
import { getDataAsString } from "../../data.js";
|
|
8
|
-
import i18n from "../../i18n.js";
|
|
9
|
-
import {generateLimiter} from "../user.js";
|
|
10
|
-
import rateLimit from "express-rate-limit";
|
|
11
|
-
import {maxAIReflectiveSteps} from "../../constants.js";
|
|
12
5
|
import {providers} from "./constants.js";
|
|
13
|
-
import {ChatDeepSeek} from "@langchain/deepseek";
|
|
14
|
-
import {ChatAnthropic} from "@langchain/anthropic";
|
|
15
6
|
import {Config} from "../../config.js";
|
|
7
|
+
import { Event } from "../../events.js";
|
|
8
|
+
import rateLimit from "express-rate-limit";
|
|
9
|
+
import {generateLimiter} from "../user.js";
|
|
10
|
+
import {maxAIReflectiveSteps} from "../../constants.js";
|
|
11
|
+
import i18n from "../../i18n.js";
|
|
12
|
+
import {parseSafeJSON} from "../../core.js";
|
|
16
13
|
|
|
17
14
|
let logger = null;
|
|
18
15
|
|
|
19
|
-
export const getAIProvider= (aiProvider, aiModel, apiKey)=>{
|
|
20
|
-
let llm;
|
|
16
|
+
export const getAIProvider= async (aiProvider, aiModel, apiKey)=>{
|
|
21
17
|
try {
|
|
22
18
|
switch (aiProvider) {
|
|
23
|
-
case 'OpenAI':
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
case 'OpenAI': {
|
|
20
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
21
|
+
return new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7});
|
|
22
|
+
}
|
|
23
|
+
case 'Google': {
|
|
24
|
+
const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
|
|
25
|
+
return new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7});
|
|
26
|
+
}
|
|
27
|
+
case 'DeepSeek': {
|
|
28
|
+
const { ChatDeepSeek } = await import("@langchain/deepseek");
|
|
29
|
+
return new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7});
|
|
30
|
+
}
|
|
31
|
+
case 'Anthropic': {
|
|
32
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
33
|
+
return new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7});
|
|
34
|
+
}
|
|
35
35
|
default:
|
|
36
36
|
throw new Error(`Unsupported AI provider: ${aiProvider}`);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
40
|
+
logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
|
|
41
|
+
throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
|
|
42
|
+
}
|
|
43
|
+
logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
|
|
44
|
+
throw e; // Re-throw other errors
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
export const assistantGlobalLimiter = rateLimit({
|
|
@@ -53,14 +56,40 @@ export const assistantGlobalLimiter = rateLimit({
|
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
async function searchModels(query, user) {
|
|
56
|
-
if (!query) return [];
|
|
59
|
+
if (!query) return { main: [], related: [] };
|
|
57
60
|
const searchRegex = new RegExp(query, 'i');
|
|
58
|
-
|
|
61
|
+
const mainModels = await modelsCollection.find({
|
|
59
62
|
$or: [{ _user: user.username }, { _user: { $exists: false } }],
|
|
60
63
|
$and: [{ $or: [{ name: { $regex: searchRegex } }, { description: { $regex: searchRegex } }] }]
|
|
61
64
|
}, {
|
|
62
65
|
projection: { name: 1, description: 1, fields: 1, _id: 0 }
|
|
63
66
|
}).limit(10).toArray();
|
|
67
|
+
|
|
68
|
+
if (mainModels.length === 0) {
|
|
69
|
+
return { main: [], related: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const relatedModelNames = new Set();
|
|
73
|
+
mainModels.forEach(model => {
|
|
74
|
+
model.fields.forEach(field => {
|
|
75
|
+
if (field.type === 'relation' && field.relation) {
|
|
76
|
+
relatedModelNames.add(field.relation);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const mainModelNames = new Set(mainModels.map(m => m.name));
|
|
82
|
+
const finalRelatedModelNames = [...relatedModelNames].filter(name => !mainModelNames.has(name));
|
|
83
|
+
|
|
84
|
+
let relatedModels = [];
|
|
85
|
+
if (finalRelatedModelNames.length > 0) {
|
|
86
|
+
relatedModels = await modelsCollection.find({
|
|
87
|
+
name: { $in: finalRelatedModelNames },
|
|
88
|
+
$or: [{ _user: user.username }, { _user: { $exists: false } }]
|
|
89
|
+
}, { projection: { name: 1, description: 1, fields: 1, _id: 0 } }).toArray();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { main: mainModels, related: relatedModels };
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
const createSystemPrompt = (modelDefs, lang) => {
|
|
@@ -87,7 +116,7 @@ Tu as accès aux outils et actions suivants.
|
|
|
87
116
|
|
|
88
117
|
OUTILS DE RAISONNEMENT INTERNE: (Utilisés pour collecter de l'information avant de décider de l'action finale)
|
|
89
118
|
1. **search_models**: Pour rechercher les modèles de données disponibles.
|
|
90
|
-
- Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }
|
|
119
|
+
- Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }. Il retourne aussi la structure des modèles directement liés.
|
|
91
120
|
====
|
|
92
121
|
|
|
93
122
|
ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat à l'utilisateur)
|
|
@@ -119,9 +148,24 @@ ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat
|
|
|
119
148
|
- \`yAxis\` (string): (Optionnel, sauf pour sum/avg/min/max) Le champ numérique à agréger du modèle.
|
|
120
149
|
- \`filter\` (object): (Optionnel, filtre de la recherche, même écriture stricte que pour les filtres de recherche (voir plus bas pour les exemples)
|
|
121
150
|
|
|
151
|
+
7. **generateHtmlView**: Pour créer une vue personnalisée en utilisant un template HTML.
|
|
152
|
+
- Utilisation: { "action": "generateHtmlView", "params": { ...config } }
|
|
153
|
+
- Le paramètre \`config\` doit contenir :
|
|
154
|
+
- \`title\` (string): Un titre pour la vue.
|
|
155
|
+
- \`model\` (string): Le nom du modèle de données à utiliser.
|
|
156
|
+
- \`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.**
|
|
157
|
+
Pour une liste, tu DOIS utiliser une boucle \`{{#each data}}...{{/each}}\`.
|
|
158
|
+
À l'intérieur d'une boucle, accède aux champs avec \`{{this.fieldName}}\`.
|
|
159
|
+
Pour les champs de type \`string_t\` ou \`richtext_t\`, accède à la traduction avec \`{{this.fieldName.value}}\`.
|
|
160
|
+
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').
|
|
161
|
+
- \`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.**
|
|
162
|
+
- \`filter\` (object): (Optionnel) Un filtre pour sélectionner les documents à afficher.
|
|
163
|
+
- \`limit\` (number): (Optionnel, défaut 10) Le nombre maximum de documents à récupérer.
|
|
164
|
+
|
|
122
165
|
Voici le mémo pour assigner des valeurs aux champs des modèles,avec ces types de données :
|
|
123
166
|
utilise une chaine de caractère convertible en ObjectId (mongodb) lorsque le nom du champ est _id
|
|
124
|
-
utilise une chaine de caracteres lorsque le type de champ est : string
|
|
167
|
+
utilise une chaine de caracteres lorsque le type de champ est : string , password, url, phone, email, richtext
|
|
168
|
+
utilise un objet { key: "trKey", value: "Translation"} lorsque le champ est string_t
|
|
125
169
|
utilise un filtre en retour si le type de champ est code et language='json' et conditionBuilder=true
|
|
126
170
|
utilise une chaine si c'est un type de champ code par défaut.
|
|
127
171
|
utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
|
|
@@ -135,13 +179,11 @@ utilise un tableau d'_ids pour remplir les champs relation multiple=true
|
|
|
135
179
|
utilise la valeur en héxadecimal, ex: '#FF0000' pour les champs de type : color
|
|
136
180
|
utilise les valeurs de cron standard '* * * * * *' pour : cronSchedule
|
|
137
181
|
|
|
138
|
-
PROCESSUS DE RAISONNEMENT:
|
|
139
|
-
a- L'utilisateur pose une question
|
|
140
|
-
b-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Si tu n'as aucune commande à exécuter directement, réponds simplement à l'utilisateur avec "displayMessage".
|
|
144
|
-
|
|
182
|
+
PROCESSUS DE RAISONNEMENT STRICT:
|
|
183
|
+
a- L'utilisateur pose une question.
|
|
184
|
+
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.
|
|
185
|
+
c- **Étape 2 (Obligatoire):** Analyse la réponse de \`search_models\` que le système t'a fournie.
|
|
186
|
+
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.
|
|
145
187
|
CONTEXTE ACTUEL:
|
|
146
188
|
- Date du jour de la conversation : ${dt}
|
|
147
189
|
- La langue ISO à utiliser dans la conversation : ${lang}
|
|
@@ -183,6 +225,67 @@ COMMANDE FINALE :
|
|
|
183
225
|
}
|
|
184
226
|
}
|
|
185
227
|
|
|
228
|
+
Question: Crée un tableau de bord des rôles et permissions avec un design sophistiqué.
|
|
229
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "role" } }
|
|
230
|
+
COMMANDE FINALE :
|
|
231
|
+
{
|
|
232
|
+
"action": "generateHtmlView",
|
|
233
|
+
"params": {
|
|
234
|
+
"title": "Matrice des Rôles et Permissions",
|
|
235
|
+
"model": "role",
|
|
236
|
+
"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>",
|
|
237
|
+
"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}",
|
|
238
|
+
"filter": {},
|
|
239
|
+
"limit": 10
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Question: Affiche une carte de visite stylisée pour le premier contact.
|
|
244
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "contact" } }
|
|
245
|
+
COMMANDE FINALE :
|
|
246
|
+
{
|
|
247
|
+
"action": "generateHtmlView",
|
|
248
|
+
"params": {
|
|
249
|
+
"title": "Carte de Visite",
|
|
250
|
+
"model": "contact",
|
|
251
|
+
"template": "<div class='card-container'><h3>{{this.firstName}} {{this.lastName}}</h3><p>{{this.email}}</p></div>",
|
|
252
|
+
"css": "#{{containerId}} .card-container { border: 1px solid #ccc; border-radius: 8px; padding: 16px; background-color: #f9f9f9; } #{{containerId}} h3 { margin-top: 0; color: #333; }",
|
|
253
|
+
"filter": {},
|
|
254
|
+
"limit": 1
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Question: Montre-moi un catalogue des produits.
|
|
259
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "product" } }
|
|
260
|
+
COMMANDE FINALE :
|
|
261
|
+
{
|
|
262
|
+
"action": "generateHtmlView",
|
|
263
|
+
"params": {
|
|
264
|
+
"title": "Catalogue Produits",
|
|
265
|
+
"model": "product",
|
|
266
|
+
"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>",
|
|
267
|
+
"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}",
|
|
268
|
+
"filter": {},
|
|
269
|
+
"limit": 12
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Question: Affiche-moi les dernières requêtes API avec un design futuriste.
|
|
274
|
+
Ta réponse: { "action" : "search_models", "params": { "query": "request" } }
|
|
275
|
+
COMMANDE FINALE :
|
|
276
|
+
{
|
|
277
|
+
"action": "generateHtmlView",
|
|
278
|
+
"params": {
|
|
279
|
+
"title": "Journal des Requêtes API",
|
|
280
|
+
"model": "request",
|
|
281
|
+
"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>",
|
|
282
|
+
"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}",
|
|
283
|
+
"filter": {},
|
|
284
|
+
"sort": { "_id": -1 },
|
|
285
|
+
"limit": 12
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
186
289
|
Absolute rules:
|
|
187
290
|
====
|
|
188
291
|
NEVER use the non-aggregated syntax of the MongoDB operators :
|
|
@@ -217,14 +320,10 @@ COMMANDE FINALE :
|
|
|
217
320
|
* @returns {object} Le filtre corrigé.
|
|
218
321
|
*/
|
|
219
322
|
function correctAIFilter(filter) {
|
|
220
|
-
if (!filter || typeof filter !== 'object' || Array.isArray(filter))
|
|
221
|
-
return filter; // Pas un objet à corriger
|
|
222
|
-
}
|
|
323
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) return filter;
|
|
223
324
|
|
|
224
325
|
const keys = Object.keys(filter);
|
|
225
|
-
if (keys.length <= 1)
|
|
226
|
-
return filter; // Rien à corriger
|
|
227
|
-
}
|
|
326
|
+
if (keys.length <= 1) return filter;
|
|
228
327
|
|
|
229
328
|
// Vérifie si toutes les clés sont des opérateurs (commencent par '$')
|
|
230
329
|
const allKeysAreOperators = keys.every(key => key.startsWith('$'));
|
|
@@ -235,7 +334,7 @@ function correctAIFilter(filter) {
|
|
|
235
334
|
return { '$and': andConditions };
|
|
236
335
|
}
|
|
237
336
|
|
|
238
|
-
return filter;
|
|
337
|
+
return filter;
|
|
239
338
|
}
|
|
240
339
|
|
|
241
340
|
/**
|
|
@@ -250,36 +349,18 @@ async function executeTool(action, params, user, allModels) {
|
|
|
250
349
|
logger.debug(`[Assistant] Exécution de l'outil: ${action} avec les paramètres:`, params);
|
|
251
350
|
try {
|
|
252
351
|
switch (action) {
|
|
253
|
-
case 'search': {
|
|
254
|
-
const modelDef = allModels.find(m => m.name === params.model);
|
|
255
|
-
if (!modelDef) {
|
|
256
|
-
return `Erreur: Le modèle '${params.model}' n'a pas été trouvé. Impossible de lancer la recherche.`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const searchResult = await searchData({
|
|
260
|
-
model: params.model,
|
|
261
|
-
filter: params.filter,
|
|
262
|
-
limit: params.limit || 10,
|
|
263
|
-
sort: params.sort
|
|
264
|
-
}, user);
|
|
265
|
-
|
|
266
|
-
if (searchResult.data.length === 0) {
|
|
267
|
-
return i18n.t('assistant.noResults', "Aucun résultat trouvé.");
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const resultString = i18n.t('assistant.searchResults', "Voici les résultats :") +
|
|
271
|
-
searchResult.data.map(item =>
|
|
272
|
-
`\n- ${getDataAsString(modelDef, item, { i18n, t: i18n.t }, allModels, true)}`
|
|
273
|
-
).join('');
|
|
274
|
-
|
|
275
|
-
return resultString;
|
|
276
|
-
}
|
|
277
352
|
case 'search_models': {
|
|
278
|
-
const foundModels = await searchModels(params.query, user);
|
|
353
|
+
const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
|
|
279
354
|
|
|
280
355
|
if (foundModels.length > 0) {
|
|
281
|
-
|
|
356
|
+
let responseText = "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
|
|
282
357
|
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('');
|
|
358
|
+
|
|
359
|
+
if (relatedModels.length > 0) {
|
|
360
|
+
responseText += "\n\nPour votre information, voici la structure des modèles liés que vous pouvez utiliser dans les templates :";
|
|
361
|
+
responseText += relatedModels.map(m => `\n- Modèle lié "${m.name}":\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
|
|
362
|
+
}
|
|
363
|
+
return responseText;
|
|
283
364
|
} else {
|
|
284
365
|
return "Je n'ai trouvé aucun modèle correspondant à votre recherche.";
|
|
285
366
|
}
|
|
@@ -296,16 +377,13 @@ async function executeTool(action, params, user, allModels) {
|
|
|
296
377
|
/**
|
|
297
378
|
* Gère la requête de chat, soit en exécutant une action confirmée,
|
|
298
379
|
* soit en lançant la boucle de raisonnement de l'IA.
|
|
299
|
-
|
|
300
|
-
* @param {Array} history - L'historique de la conversation.
|
|
301
|
-
* @param {string} provider - Le fournisseur d'IA ('OpenAI' ou 'google').
|
|
302
|
-
* @param {object} context - Contexte additionnel.
|
|
380
|
+
@param {object} params - La liste des paramètres envoyés par la requete
|
|
303
381
|
* @param {object} user - L'objet utilisateur.
|
|
304
|
-
* @param {object} confirmedAction - Une action pré-approuvée par l'utilisateur.
|
|
305
382
|
* @returns {Promise<object>} La réponse de l'assistant.
|
|
306
383
|
*/
|
|
307
|
-
async function handleChatRequest(
|
|
384
|
+
export async function handleChatRequest(params, user) {
|
|
308
385
|
|
|
386
|
+
const { message, history, provider, confirmedAction } = params;
|
|
309
387
|
const allModels = await modelsCollection.find({$or: [{_user: {$exists: false}}, {_user: user.username}]}).toArray();
|
|
310
388
|
|
|
311
389
|
// --- GESTION D'UNE ACTION CONFIRMÉE ---
|
|
@@ -329,8 +407,9 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
329
407
|
|
|
330
408
|
// --- INITIALISATION DE L'IA ---
|
|
331
409
|
let llm;
|
|
410
|
+
let llmOptions = { };
|
|
332
411
|
try {
|
|
333
|
-
const p = provider ||
|
|
412
|
+
const p = Config.Get('assistant.provider', provider ||'OpenAI');
|
|
334
413
|
const envKeyName = providers[p].key;
|
|
335
414
|
if (!envKeyName) return {success: false, message: `Fournisseur IA non supporté : ${p}`};
|
|
336
415
|
|
|
@@ -340,15 +419,17 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
340
419
|
|
|
341
420
|
if (!apiKey) return {success: false, message: `Clé API pour ${p} (${envKeyName}) non trouvée.`};
|
|
342
421
|
|
|
343
|
-
|
|
422
|
+
const model = Config.Get('assistant.model', providers[p]?.defaultModel);
|
|
423
|
+
llmOptions = { provider: p, model, apiKey };
|
|
424
|
+
llm = await getAIProvider(llmOptions.provider, llmOptions.model, llmOptions.apiKey);
|
|
344
425
|
} catch (initError) {
|
|
345
426
|
logger.error(`[Assistant] Erreur d'initialisation du client IA: ${initError.message}`);
|
|
346
427
|
return {success: false, message: `Erreur d'initialisation du client IA: ${initError.message}`};
|
|
347
428
|
}
|
|
348
429
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const conversationHistory = history
|
|
430
|
+
const systemPrompt = await Event.Trigger('OnSystemPrompt', 'event', 'user', user);
|
|
431
|
+
|
|
432
|
+
const conversationHistory = (history || [])
|
|
352
433
|
.filter(msg => msg.text && !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
|
|
353
434
|
.map(msg => new (msg.from === 'user' ? HumanMessage : SystemMessage)(msg.text));
|
|
354
435
|
|
|
@@ -367,80 +448,56 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
367
448
|
// Parsing JSON robuste
|
|
368
449
|
let parsedResponse;
|
|
369
450
|
try {
|
|
451
|
+
// Tente d'extraire le JSON de la réponse, même s'il est entouré de texte.
|
|
452
|
+
const jsonRegex = /\{[\s\S]*\}/s; // 's' flag pour que '.' matche les nouvelles lignes
|
|
453
|
+
const match = llmOutput.match(jsonRegex);
|
|
370
454
|
|
|
371
|
-
|
|
455
|
+
if (match && match[0]) {
|
|
456
|
+
// Si un JSON est trouvé, on tente de le parser
|
|
457
|
+
parsedResponse = parseSafeJSON(match[0]);
|
|
458
|
+
} else {
|
|
459
|
+
// Aucun JSON trouvé, c'est probablement une réponse textuelle simple.
|
|
460
|
+
return { success: true, displayMessage: llmOutput };
|
|
461
|
+
}
|
|
372
462
|
|
|
373
463
|
if (!parsedResponse.action || !parsedResponse.params) {
|
|
374
464
|
throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
|
|
375
465
|
}
|
|
376
466
|
} catch (parseError) {
|
|
377
467
|
logger.error(`[Assistant] Erreur de parsing de la réponse de l'IA: ${parseError.message}. Réponse brute: "${llmOutput}"`);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
displayMessage: llmOutput || i18n.t('assistant.invalidResponse', "Désolé, je n'ai pas pu formuler une réponse correcte. Veuillez réessayer.")
|
|
381
|
-
};
|
|
468
|
+
// Si le parsing échoue, on renvoie le message brut de l'IA, qui est peut-être une réponse textuelle valide.
|
|
469
|
+
return { success: true, displayMessage: llmOutput };
|
|
382
470
|
}
|
|
383
471
|
|
|
384
472
|
logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
|
|
385
473
|
conversationHistory.push(new SystemMessage(JSON.stringify(parsedResponse)));
|
|
386
474
|
|
|
387
|
-
const { action, params } = parsedResponse;
|
|
475
|
+
const { action, params:parsedParams } = parsedResponse;
|
|
388
476
|
|
|
389
477
|
// Correction automatique du filtre généré par l'IA, qui hallucine parfois
|
|
390
|
-
if (
|
|
391
|
-
logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(
|
|
392
|
-
|
|
478
|
+
if (parsedParams && parsedParams.filter) {
|
|
479
|
+
logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(parsedParams.filter)}`);
|
|
480
|
+
parsedParams.filter = correctAIFilter(parsedParams.filter);
|
|
393
481
|
}
|
|
394
482
|
|
|
395
|
-
//
|
|
396
|
-
if (action
|
|
397
|
-
|
|
398
|
-
|
|
483
|
+
// Outils pour le raisonnement interne de l'IA
|
|
484
|
+
if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
|
|
485
|
+
const toolResult = await executeTool(action, parsedParams, user, allModels);
|
|
486
|
+
conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
|
|
487
|
+
continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
|
|
399
488
|
}
|
|
400
489
|
|
|
401
|
-
|
|
402
|
-
if (action === 'search') {
|
|
403
|
-
const searchResult = await searchData({
|
|
404
|
-
model: params.model,
|
|
405
|
-
filter: params.filter,
|
|
406
|
-
limit: params.limit || 10,
|
|
407
|
-
sort: params.sort
|
|
408
|
-
}, user);
|
|
409
|
-
if (searchResult.data?.length > 0) {
|
|
410
|
-
return { success: true, dataResult: { model: params.model, data: searchResult.data } };
|
|
411
|
-
} else {
|
|
412
|
-
return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
|
|
413
|
-
}
|
|
414
|
-
}
|
|
490
|
+
const res = await Event.Trigger('OnChatAction', 'event', 'user', action, params, parsedResponse, llmOptions, user);
|
|
415
491
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
492
|
+
if( !res ) {
|
|
493
|
+
// Si l'action n'est reconnue par aucune des logiques ci-dessus
|
|
494
|
+
logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
|
|
419
495
|
return {
|
|
420
496
|
success: true,
|
|
421
|
-
displayMessage:
|
|
422
|
-
confirmationRequest: parsedResponse
|
|
497
|
+
displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", {action})
|
|
423
498
|
};
|
|
424
499
|
}
|
|
425
|
-
|
|
426
|
-
// Action finale pour afficher un message
|
|
427
|
-
if (action === 'displayMessage') {
|
|
428
|
-
return { success: true, displayMessage: params.message };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Outils pour le raisonnement interne de l'IA
|
|
432
|
-
if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
|
|
433
|
-
const toolResult = await executeTool(action, params, user, allModels);
|
|
434
|
-
conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
|
|
435
|
-
continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Si l'action n'est reconnue par aucune des logiques ci-dessus
|
|
439
|
-
logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
|
|
440
|
-
return {
|
|
441
|
-
success: true,
|
|
442
|
-
displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", { action })
|
|
443
|
-
};
|
|
500
|
+
return res;
|
|
444
501
|
}
|
|
445
502
|
|
|
446
503
|
// Si la boucle se termine sans une action finale
|
|
@@ -473,12 +530,74 @@ async function executeConfirmedAction(action, params, user) {
|
|
|
473
530
|
}
|
|
474
531
|
}
|
|
475
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Gère une action finale décidée par l'IA et retourne une réponse pour le client.
|
|
535
|
+
* Cette fonction est appelée via le système d'événements 'OnChatAction'.
|
|
536
|
+
* @param {string} action - L'action à exécuter.
|
|
537
|
+
* @param {object} params - Les paramètres de l'action.
|
|
538
|
+
* @param {object} parsedResponse - La réponse JSON originale de l'IA, pour les demandes de confirmation.
|
|
539
|
+
* @param {object} user - L'objet utilisateur.
|
|
540
|
+
* @returns {Promise<object|boolean>} - L'objet de réponse final ou false si l'action n'est pas reconnue.
|
|
541
|
+
*/
|
|
542
|
+
async function handleFinalChatAction(action, params, parsedResponse, user) {
|
|
543
|
+
// Action de génération de graphique, gérée par le front-end
|
|
544
|
+
if (action === 'generateChart') {
|
|
545
|
+
return {success: true, chartConfig: params};
|
|
546
|
+
}
|
|
547
|
+
// Action de génération de vue HTML
|
|
548
|
+
if (action === 'generateHtmlView') {
|
|
549
|
+
const viewData = await searchData({
|
|
550
|
+
model: params.model,
|
|
551
|
+
filter: params.filter,
|
|
552
|
+
limit: params.limit || 10,
|
|
553
|
+
depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
|
|
554
|
+
}, user);
|
|
555
|
+
|
|
556
|
+
if (viewData.data.length === 0) {
|
|
557
|
+
return {
|
|
558
|
+
success: true,
|
|
559
|
+
displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.")
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return {success: true, htmlViewConfig: {...params, data: viewData.data}};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Action de recherche à afficher, gérée par le front-end
|
|
566
|
+
if (action === 'search') {
|
|
567
|
+
const searchResult = await searchData({ model: params.model, filter: params.filter, limit: params.limit || 10, sort: params.sort }, user);
|
|
568
|
+
if (searchResult.data?.length > 0) {
|
|
569
|
+
return {success: true, dataResult: {model: params.model, data: searchResult.data}};
|
|
570
|
+
} else {
|
|
571
|
+
return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Actions nécessitant une confirmation de l'utilisateur
|
|
576
|
+
if (['post', 'update', 'delete'].includes(action)) {
|
|
577
|
+
return { success: true, displayMessage: i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :"), confirmationRequest: parsedResponse };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Action finale pour afficher un message
|
|
581
|
+
if (action === 'displayMessage') {
|
|
582
|
+
return {success: true, displayMessage: params.message};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return false; // Action non reconnue par cet écouteur
|
|
586
|
+
}
|
|
587
|
+
|
|
476
588
|
|
|
477
589
|
export async function onInit(engine) {
|
|
478
590
|
logger = engine.getComponent(Logger);
|
|
479
591
|
|
|
480
592
|
const {middlewareAuthenticator, userInitiator} = await import('../user.js');
|
|
481
593
|
|
|
594
|
+
Event.Listen('OnSystemPrompt', (user) => {
|
|
595
|
+
return createSystemPrompt([], user.lang || 'en');
|
|
596
|
+
}, 'event', 'user');
|
|
597
|
+
// Enregistre le gestionnaire central pour les actions finales du chat.
|
|
598
|
+
// D'autres modules pourraient également écouter cet événement pour étendre les fonctionnalités.
|
|
599
|
+
Event.Listen('OnChatAction', handleFinalChatAction,"event", "user");
|
|
600
|
+
|
|
482
601
|
engine.post('/api/assistant/chat', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter], async (req, res) => {
|
|
483
602
|
// On récupère TOUTES les propriétés du body, y compris l'action confirmée
|
|
484
603
|
const {message, history, provider, context, confirmedAction} = req.fields;
|
|
@@ -500,7 +619,7 @@ export async function onInit(engine) {
|
|
|
500
619
|
}
|
|
501
620
|
|
|
502
621
|
try {
|
|
503
|
-
const result = await handleChatRequest(
|
|
622
|
+
const result = await handleChatRequest(req.fields, req.me);
|
|
504
623
|
res.json(result);
|
|
505
624
|
} catch (error) {
|
|
506
625
|
logger.error(`[Endpoint /api/assistant/chat] Erreur inattendue: ${error.message}`, error.stack);
|
package/src/modules/bucket.js
CHANGED
|
@@ -6,7 +6,7 @@ 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";
|
|
@@ -38,6 +38,7 @@ export const requestRestore = async (user, lang) => {
|
|
|
38
38
|
content: i18n.t('email.backup.restoreRequest.content', {
|
|
39
39
|
user: user?.username,
|
|
40
40
|
fullToken: fullRestoreToken,
|
|
41
|
+
host: getHost(),
|
|
41
42
|
modelsToken: modelsRestoreToken
|
|
42
43
|
})
|
|
43
44
|
});
|