L4.2 : paramètre application sur les outils AD et workflow (D26)
L'application venait de WMS_APPLICATION (partagée par tous les profils) : le MCP n'interrogeait que EasyWMS, alors que CustomApp porte le spécifique client (153 workflows CST_ sur ce tenant) et que 9 applications sont déclarées par Application/GetAll. Paramètre application (défaut : l'application du profil, comportement inchangé sans lui) sur get_ad_elements, search_ad_elements, get_ad_element_details, search_workflows, get_workflow_details, list_workflow_categories. Clés de cache : ad-service passe par (application, type), workflow-service par application — sans quoi un appel CustomApp polluerait le cache EasyWMS. L'invalidation reste l'abonnement onSwitch (D8), le chargement reste paresseux (aucun préchargement des 9 applications, D10). list_workflow_categories s'adosse à Application/GetAll (liste allégée en cache : le blob data de chaque application pèse ~100 Ko) ; get_application_summary regroupe par application et ne détaille que les entrées en cache (D24), workflows compris. Acté en D26 ; CLAUDE.md mis à jour (Caches, AD), L4.2 retiré de la ROADMAP. Vérifications rejouées via le protocole (LIMAGRAIN / LIMAGRAI2512), requêtes séquentielles : - search_workflows(CST_, application:CustomApp) -> 5 objets peuplés dont CST_SendRejectContainersToPK ([Workflow] Successfully cached 153 workflows for "CustomApp"). - get_ad_elements(Workflow, application:CustomApp) -> count 153, éléments CST_* ([AD] Successfully cached 153 CustomApp::Workflow). - Séquence EasyWMS -> CustomApp -> EasyWMS sur search_workflows : 4012 vs 153, retour en cache hit ([Workflow] Using cached data for "EasyWMS"), aucune pollution ; get_application_summary montre les deux caches (workflowCachesByApplication EasyWMS 4012 / CustomApp 153). - Sans paramètre application -> comportement inchangé (W1 = W3). - switch_wms_profile EUROTRAFIC puis retour -> [Workflow] Cache cleared, [AD] All caches invalidated, [EntityResolver] Cache cleared ; summary vide. - list_workflow_categories -> 9 applications, comptes réels des applications chargées. Baseline : tools/list 23 (les 23 noms répondent), resources 6, rejet D23 d'un paramètre inconnu OK (search_workflows/applikation), npm test 4/4 exit 0. Anomalie hors périmètre consignée dans ROADMAP.md : get_workflow_details peut dépasser le seuil de rejet client (~101 800 caractères mesurés sur CST_SendRejectContainersToPK), comportement antérieur au lot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -170,13 +170,18 @@ disponibles : c'est ainsi que Claude sait appeler `switch_wms_profile`.
|
||||
|
||||
## Caches
|
||||
|
||||
Deux caches, TTL commun `WORKFLOW_CACHE_TTL` (3 600 000 ms), chargement
|
||||
paresseux, vidés à chaque bascule de profil (D10).
|
||||
TTL commun `WORKFLOW_CACHE_TTL` (3 600 000 ms), chargement paresseux, vidés à
|
||||
chaque bascule de profil (D10). Les outils AD et workflow acceptent un
|
||||
paramètre **`application`** (défaut : l'application du profil) — les clés de
|
||||
cache incluent l'application pour éviter toute pollution croisée (D26).
|
||||
|
||||
| Cache | Granularité | Pagination |
|
||||
|---|---|---|
|
||||
| `workflow-service` | global (~3 700 workflows) | `WORKFLOW_PAGE_SIZE`, 5000 |
|
||||
| `ad-service` | **un par type** (20 types) | `AD_ELEMENT_TYPES` : `View` 200, `Workflow` 5000, `Resource` 15000, autres 100000 |
|
||||
| `workflow-service` | **un par application** (~4 000 EasyWMS, 153 CustomApp) + liste allégée d'`Application/GetAll` | `WORKFLOW_PAGE_SIZE`, 5000 |
|
||||
| `ad-service` | **un par (application, type)** (20 types) | `AD_ELEMENT_TYPES` : `View` 200, `Workflow` 5000, `Resource` 15000, autres 100000 |
|
||||
|
||||
**Ne préchargez jamais les 9 applications** : seule l'application demandée est
|
||||
chargée (D26).
|
||||
|
||||
Les tailles de page par type viennent de l'observation des timeouts serveur —
|
||||
ne les augmentez pas à l'aveugle.
|
||||
@@ -235,11 +240,13 @@ pluriel), `Tasks`, `Stocks`, `ProductLocations`, `InboundOrders`, `Receptions`,
|
||||
confondues) s'obtient par `get_entity_metadata` (API Metadata) — le catalogue
|
||||
de la resource `wms://entities` est un raccourci de confort, pas la référence.
|
||||
|
||||
**Application Dictionary** : 20 types, ~38 800 éléments. `Resource` (29 374) est
|
||||
de loin le plus lourd ; 3 types sont valides mais vides (`Dashboard`,
|
||||
`TimelineTemplate`, `Toggle`). `WorkflowAction` et `WritingModel` ont été
|
||||
retirés — 404 (D17). Détail :
|
||||
[docs/ad-api-validation.md](docs/ad-api-validation.md).
|
||||
**Application Dictionary** : 20 types, ~38 800 éléments (sur `EasyWMS`).
|
||||
`Resource` (29 374) est de loin le plus lourd ; 3 types sont valides mais vides
|
||||
(`Dashboard`, `TimelineTemplate`, `Toggle`). `WorkflowAction` et `WritingModel`
|
||||
ont été retirés — 404 (D17). Détail :
|
||||
[docs/ad-api-validation.md](docs/ad-api-validation.md). 9 applications AD sont
|
||||
déclarées ; **`CustomApp` porte le spécifique client** (workflows `CST_*`) et
|
||||
s'interroge via le paramètre `application` des outils AD et workflow (D26).
|
||||
|
||||
**Paramètres système** : pas d'entité `CommandParameterData`. La configuration
|
||||
se lit dans `Parameter` (+ `DefaultValue`) et `ParamValue` (surcharges par
|
||||
|
||||
@@ -519,3 +519,52 @@ Modalités :
|
||||
repli « Metadata injoignable »), car le modèle Writing/Metrics peut contenir
|
||||
des entités hors Reading. Le `warning` est conservé aussi dans la réponse
|
||||
d'erreur si le WMS échoue ensuite.
|
||||
|
||||
---
|
||||
|
||||
## D26 — Paramètre `application` : caches par application, chargement toujours paresseux
|
||||
|
||||
**Contexte (mesures des 24-25/08/2026, `LIMAGRAI2512`).** L'application
|
||||
interrogée venait de `WMS_APPLICATION` (partagée par tous les profils) : le MCP
|
||||
ne voyait que `EasyWMS`. Or `POST /AD/api/Application/GetAll` déclare **9
|
||||
applications**, et **CustomApp porte le spécifique client** (153 workflows
|
||||
`CST_*` sur ce tenant) — précisément ce qu'on cherche en debug. Les 11 entités
|
||||
`CustomApp` ne sont requêtables dans aucun contexte : l'API AD est le seul
|
||||
accès au spécifique client.
|
||||
|
||||
**Décision.** Un paramètre `application` (défaut : l'application du profil,
|
||||
donc comportement strictement inchangé sans lui) sur six outils :
|
||||
`get_ad_elements`, `search_ad_elements`, `get_ad_element_details`,
|
||||
`search_workflows`, `get_workflow_details`, `list_workflow_categories`.
|
||||
|
||||
**Contrat de cache.**
|
||||
|
||||
| Service | Clé avant | Clé après |
|
||||
|---|---|---|
|
||||
| `ad-service` | un cache par type | un cache par **(application, type)** (`app::type`) |
|
||||
| `workflow-service` | un cache global | un cache par **application** |
|
||||
|
||||
Sans ces clés, un appel CustomApp polluerait le cache EasyWMS du même type.
|
||||
Règles associées :
|
||||
|
||||
- **L'invalidation reste l'abonnement `onSwitch()`** (D8) : la bascule de
|
||||
profil vide **tous** les caches, toutes applications confondues. Aucune
|
||||
invalidation manuelle inter-module.
|
||||
- **Pas de préchargement des 9 applications** (D10) : seule l'application
|
||||
effectivement demandée est chargée — le type `Resource` pèse 29 374 éléments
|
||||
sur la seule EasyWMS.
|
||||
- `workflow-service` cache aussi la liste de `Application/GetAll`, **allégée**
|
||||
(`name`, `id`, `version`) : chaque élément de la réponse brute embarque un
|
||||
blob `data` de ~100 Ko (la définition EasyBuilder complète) qu'on ne
|
||||
conserve pas.
|
||||
- `list_workflow_categories` est adossé à `Application/GetAll` (les 9
|
||||
applications) et non plus aux `applicationName` du seul cache actif. La note
|
||||
de L1.3 reste vraie — pas de champ catégorie ; les comptes de workflows ne
|
||||
sont affichés que pour les applications déjà chargées (paresseux). Le
|
||||
paramètre `category` de `search_workflows` (filtre sur `applicationName`)
|
||||
subsiste : `application` choisit le jeu chargé, `category` filtre dedans —
|
||||
leur articulation est documentée dans les descriptions.
|
||||
- `get_application_summary` regroupe l'état par application puis par type et
|
||||
ne détaille que les entrées **effectivement en cache** : la sortie reste
|
||||
bornée quel que soit le nombre d'applications interrogées (D24). Il expose
|
||||
aussi les caches de workflows par application.
|
||||
|
||||
+7
-52
@@ -25,58 +25,6 @@ exploration à part — c'est probablement là que vivent les données agrégée
|
||||
produites par les jobs `MetricGatherer`. Livrable : un rapport, pas du code
|
||||
(même phase d'investigation que L4.4).
|
||||
|
||||
### L4.2 — Une seule application sur neuf est visible
|
||||
|
||||
`Application` vient de `WMS_APPLICATION` dans `.env`, **partagé par tous les
|
||||
profils**, sans surcharge par appel ni paramètre d'outil. Le MCP n'interroge donc
|
||||
jamais que `EasyWMS`.
|
||||
|
||||
`POST /AD/api/Application/GetAll` en déclare **9** :
|
||||
|
||||
| Application | Workflows | Queries | Entities |
|
||||
|---|---:|---:|---:|
|
||||
| EasyWMS | 4012 | 2239 | 338 |
|
||||
| **CustomApp** | **153** | **54** | **11** |
|
||||
| AGV | 71 | 14 | 5 |
|
||||
| Notifications | 26 | 35 | 24 |
|
||||
| GalileoFaults | 9 | 20 | 24 |
|
||||
| Common | 1 | 7 | 25 |
|
||||
| SmartUI, User, WarehouseWebDesigner | 0 | 0–8 | 0 |
|
||||
|
||||
**CustomApp porte le spécifique client** — ses workflows sont préfixés `CST_`
|
||||
(`CST_SendRejectContainersToPK`, `CST_Task`, `CST_Container`…). C'est
|
||||
précisément ce qu'on cherche en debug, et c'est aujourd'hui invisible. Au total
|
||||
**260 workflows et ~130 queries** hors périmètre.
|
||||
|
||||
Deux chantiers de difficulté très différentes :
|
||||
|
||||
**API AD — simple.** L'application est un champ du payload
|
||||
(`[application, tenant, pageSize, offset]`). Vérifié : `["CustomApp", tenant,
|
||||
5, 0]` sur `/Workflow/GetByApplication` renvoie bien les workflows `CST_`. Il
|
||||
suffit d'un paramètre `application` sur les outils AD et workflow, avec une clé
|
||||
de cache incluant l'application (sinon un cache pollué mélange les
|
||||
applications).
|
||||
|
||||
**QueryExecute — tranché : le champ `Application` ne partitionne rien.**
|
||||
`Context.AgvTasks` (entité de l'application AGV) répond aussi bien avec
|
||||
`Application: "AGV"` qu'avec `Application: "EasyWMS"`. Le contexte de lecture est
|
||||
**commun au tenant** : toutes les applications y déversent leurs entités.
|
||||
|
||||
Conséquence — traitée : la table de résolution (D21) **agrège le Metadata de
|
||||
toutes les applications** déployées, et non le seul `EasyWMS`. Inutile en
|
||||
revanche d'ajouter un paramètre `application` à `QueryExecute` : il ne changerait
|
||||
rien.
|
||||
|
||||
**Les entités `CustomApp` ne sont interrogeables dans aucun contexte.** Les 11
|
||||
entités `CST_` ont été testées sous les quatre `QueryType`, au singulier et au
|
||||
pluriel : échec partout, et `Metadata/Entities` comme `Metadata/EntitiesAll`
|
||||
renvoient **0 entité** pour `CustomApp`. Aucune n'est marquée
|
||||
`isDataWarehouse`. Ce sont des définitions EasyBuilder (`FromMetadata: false`)
|
||||
sans projection dans un contexte requêtable.
|
||||
|
||||
**L'API AD reste donc le seul accès au spécifique client** — ce qui rend le
|
||||
paramètre `application` sur les outils AD et workflow d'autant plus utile.
|
||||
|
||||
### L4.3 — Identifier le MCP dans les logs du WMS
|
||||
|
||||
Les requêtes du MCP apparaissent dans les logs du WMS sous
|
||||
@@ -170,6 +118,13 @@ plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et
|
||||
début de chaque appel).
|
||||
- **`select_expression`** : les projections via le paramètre `Select` provoquent
|
||||
des erreurs de compilation côté serveur (D13). Irritant principal restant.
|
||||
- **`get_workflow_details` peut dépasser le seuil de rejet client** (constaté
|
||||
le 25/08/2026, livraison du lot 4). La définition complète de
|
||||
`CST_SendRejectContainersToPK` (application `CustomApp`) fait ~101 800
|
||||
caractères via le protocole — au-delà du seuil de rejet mesuré en D24
|
||||
(~70 000). Comportement antérieur au lot 4 (les grosses définitions
|
||||
`EasyWMS` sont dans le même cas) : à borner et signaler (`truncated`/`hint`,
|
||||
D24) dans un lot futur.
|
||||
- **Déploiement SSH sur la VM** : l'exécutable est validé, la configuration SSH
|
||||
reste à faire.
|
||||
- **Historique des shipment templates** : hors de portée, les logs concernés
|
||||
|
||||
+82
-55
@@ -7,11 +7,26 @@
|
||||
const apiService = require('./api-service').getInstance();
|
||||
const profileManager = require('../config/profile-manager');
|
||||
|
||||
// Cache state - one cache per element type
|
||||
// Cache state - one cache per (application, element type) (D26)
|
||||
const cache = {};
|
||||
const cacheTimestamps = {};
|
||||
const CACHE_TTL = parseInt(process.env.WORKFLOW_CACHE_TTL) || 3600000; // 1 hour
|
||||
|
||||
/**
|
||||
* Application effective : celle demandée, sinon celle du profil actif.
|
||||
*/
|
||||
function resolveApplication(application) {
|
||||
return (application && application.trim()) || profileManager.getCurrent().application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clé de cache composite (D26) — sans elle, un appel CustomApp polluerait le
|
||||
* cache EasyWMS du même type.
|
||||
*/
|
||||
function cacheKey(application, elementType) {
|
||||
return `${application}::${elementType}`;
|
||||
}
|
||||
|
||||
// Invalidate all caches when profile changes — AD elements are per-tenant.
|
||||
profileManager.onSwitch(() => invalidateCache());
|
||||
|
||||
@@ -44,51 +59,55 @@ const AD_ELEMENT_TYPES = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if cache is valid for a given element type
|
||||
* Check if cache is valid for a given (application, element type)
|
||||
*/
|
||||
function isCacheValid(elementType) {
|
||||
if (!cache[elementType] || !cacheTimestamps[elementType]) {
|
||||
function isCacheValid(application, elementType) {
|
||||
const key = cacheKey(application, elementType);
|
||||
if (!cache[key] || !cacheTimestamps[key]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const age = now - cacheTimestamps[elementType];
|
||||
const age = now - cacheTimestamps[key];
|
||||
return age < CACHE_TTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all elements of a specific type from AD API
|
||||
* Implements lazy loading with caching and pagination
|
||||
* Implements lazy loading with caching and pagination.
|
||||
* Lazy par application (D26) : seule l'application demandée est chargée.
|
||||
*
|
||||
* @param {string} elementType - Type of element (Command, Query, Dialog, etc.)
|
||||
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||
* @returns {Promise<Array>} Array of elements
|
||||
*/
|
||||
async function getElements(elementType) {
|
||||
async function getElements(elementType, application) {
|
||||
// Validate element type
|
||||
if (!AD_ELEMENT_TYPES[elementType]) {
|
||||
throw new Error(`Unknown element type: ${elementType}. Valid types: ${Object.keys(AD_ELEMENT_TYPES).join(', ')}`);
|
||||
}
|
||||
|
||||
const app = resolveApplication(application);
|
||||
const key = cacheKey(app, elementType);
|
||||
|
||||
// Check cache
|
||||
if (isCacheValid(elementType)) {
|
||||
console.error(`[AD] Cache hit: ${elementType} (${cache[elementType].length} elements)`);
|
||||
return cache[elementType];
|
||||
if (isCacheValid(app, elementType)) {
|
||||
console.error(`[AD] Cache hit: ${key} (${cache[key].length} elements)`);
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
console.error(`[AD] Cache expired or empty, fetching ${elementType}...`);
|
||||
console.error(`[AD] Cache expired or empty, fetching ${key}...`);
|
||||
|
||||
try {
|
||||
let allElements = [];
|
||||
let offset = 0;
|
||||
const pageSize = AD_ELEMENT_TYPES[elementType];
|
||||
const profile = profileManager.getCurrent();
|
||||
const application = profile.application;
|
||||
const tenant = profile.tenant;
|
||||
const tenant = profileManager.getCurrent().tenant;
|
||||
|
||||
while (true) {
|
||||
const body = [application, tenant, pageSize, offset];
|
||||
const body = [app, tenant, pageSize, offset];
|
||||
|
||||
console.error(`[AD] Fetching ${elementType}: offset=${offset}, pageSize=${pageSize}`);
|
||||
console.error(`[AD] Fetching ${key}: offset=${offset}, pageSize=${pageSize}`);
|
||||
|
||||
// Use AD API (useAdApi=true)
|
||||
const response = await apiService.post(`/${elementType}/GetByApplication`, body, true);
|
||||
@@ -115,14 +134,14 @@ async function getElements(elementType) {
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cache[elementType] = allElements;
|
||||
cacheTimestamps[elementType] = Date.now();
|
||||
cache[key] = allElements;
|
||||
cacheTimestamps[key] = Date.now();
|
||||
|
||||
console.error(`[AD] Successfully cached ${allElements.length} ${elementType}`);
|
||||
console.error(`[AD] Successfully cached ${allElements.length} ${key}`);
|
||||
return allElements;
|
||||
} catch (error) {
|
||||
console.error(`[AD] Error fetching ${elementType}:`, error.message);
|
||||
throw new Error(`Failed to fetch ${elementType}: ${error.message}`);
|
||||
console.error(`[AD] Error fetching ${key}:`, error.message);
|
||||
throw new Error(`Failed to fetch ${elementType} for application "${app}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,9 +150,10 @@ async function getElements(elementType) {
|
||||
* @param {string} elementType - Type of element
|
||||
* @param {string} query - Search query (matches name, description, etc.)
|
||||
* @param {number} limit - Maximum results to return
|
||||
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||
*/
|
||||
async function searchElements(elementType, query, limit = 50) {
|
||||
const elements = await getElements(elementType);
|
||||
async function searchElements(elementType, query, limit = 50, application) {
|
||||
const elements = await getElements(elementType, application);
|
||||
|
||||
if (!query) {
|
||||
return elements.slice(0, limit);
|
||||
@@ -157,9 +177,10 @@ async function searchElements(elementType, query, limit = 50) {
|
||||
* Get element details by ID or name
|
||||
* @param {string} elementType - Type of element
|
||||
* @param {string|number} elementId - Element ID or name
|
||||
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||
*/
|
||||
async function getElementDetails(elementType, elementId) {
|
||||
const elements = await getElements(elementType);
|
||||
async function getElementDetails(elementType, elementId, application) {
|
||||
const elements = await getElements(elementType, application);
|
||||
|
||||
// Try to find by Id, id, Code, code, Name, or name
|
||||
const element = elements.find(e =>
|
||||
@@ -174,45 +195,49 @@ async function getElementDetails(elementType, elementId) {
|
||||
);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`${elementType} not found: ${elementId}`);
|
||||
const app = resolveApplication(application);
|
||||
throw new Error(
|
||||
`${elementType} not found: ${elementId} (application "${app}"). ` +
|
||||
`Utilisez search_ad_elements — pensez au paramètre application ` +
|
||||
`(ex: "CustomApp" pour le spécifique client).`
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application summary (count of each element type)
|
||||
* Only loads types that are already cached to avoid long wait times
|
||||
* Get application summary — état des caches par (application, type) (D26).
|
||||
* Seules les entrées effectivement en cache sont détaillées, pour rester
|
||||
* borné quel que soit le nombre d'applications interrogées (D24).
|
||||
* @returns {Object} application -> type -> { count, cacheAge }
|
||||
*/
|
||||
function getApplicationSummary() {
|
||||
const summary = {};
|
||||
const byApplication = {};
|
||||
|
||||
Object.keys(AD_ELEMENT_TYPES).forEach(type => {
|
||||
if (cache[type]) {
|
||||
summary[type] = {
|
||||
count: cache[type].length,
|
||||
cached: true,
|
||||
cacheAge: cacheTimestamps[type] ? Math.floor((Date.now() - cacheTimestamps[type]) / 1000) : null
|
||||
};
|
||||
} else {
|
||||
summary[type] = {
|
||||
count: 0,
|
||||
cached: false,
|
||||
cacheAge: null
|
||||
};
|
||||
}
|
||||
Object.keys(cache).forEach(key => {
|
||||
const [app, type] = key.split('::');
|
||||
if (!byApplication[app]) byApplication[app] = {};
|
||||
byApplication[app][type] = {
|
||||
count: cache[key].length,
|
||||
cacheAge: cacheTimestamps[key] ? Math.floor((Date.now() - cacheTimestamps[key]) / 1000) : null
|
||||
};
|
||||
});
|
||||
|
||||
return summary;
|
||||
return byApplication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for a specific type or all types
|
||||
* Invalidate cache for a specific type (across all applications) or all types
|
||||
*/
|
||||
function invalidateCache(elementType = null) {
|
||||
if (elementType) {
|
||||
delete cache[elementType];
|
||||
delete cacheTimestamps[elementType];
|
||||
Object.keys(cache)
|
||||
.filter(k => k.endsWith(`::${elementType}`))
|
||||
.forEach(k => {
|
||||
delete cache[k];
|
||||
delete cacheTimestamps[k];
|
||||
});
|
||||
console.error(`[AD] Cache invalidated: ${elementType}`);
|
||||
} else {
|
||||
Object.keys(cache).forEach(k => {
|
||||
@@ -224,17 +249,19 @@ function invalidateCache(elementType = null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache status
|
||||
* Get cache status, par application puis type (D26)
|
||||
*/
|
||||
function getCacheStatus() {
|
||||
const status = {};
|
||||
Object.keys(AD_ELEMENT_TYPES).forEach(type => {
|
||||
status[type] = {
|
||||
cached: !!cache[type],
|
||||
count: cache[type] ? cache[type].length : 0,
|
||||
timestamp: cacheTimestamps[type],
|
||||
age: cacheTimestamps[type] ? Math.floor((Date.now() - cacheTimestamps[type]) / 1000) : null,
|
||||
valid: isCacheValid(type)
|
||||
Object.keys(cache).forEach(key => {
|
||||
const [app, type] = key.split('::');
|
||||
if (!status[app]) status[app] = {};
|
||||
status[app][type] = {
|
||||
cached: true,
|
||||
count: cache[key].length,
|
||||
timestamp: cacheTimestamps[key],
|
||||
age: cacheTimestamps[key] ? Math.floor((Date.now() - cacheTimestamps[key]) / 1000) : null,
|
||||
valid: isCacheValid(app, type)
|
||||
};
|
||||
});
|
||||
return status;
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
* Workflow Service
|
||||
* Handles workflow fetching with lazy loading and caching
|
||||
* Workflows are only loaded when first requested (not at startup)
|
||||
*
|
||||
* Un cache par application (D26) : le paramètre `application` des outils
|
||||
* sélectionne l'application AD interrogée (défaut : celle du profil actif).
|
||||
*/
|
||||
|
||||
const apiService = require('./api-service').getInstance();
|
||||
const profileManager = require('../config/profile-manager');
|
||||
|
||||
// Cache state
|
||||
let workflowCache = null;
|
||||
let cacheTimestamp = null;
|
||||
// Cache state — un cache de workflows par application (D26)
|
||||
let workflowCaches = {}; // application -> workflows[]
|
||||
let cacheTimestamps = {}; // application -> timestamp
|
||||
let applicationsCache = null; // liste allégée de POST /Application/GetAll
|
||||
let applicationsTimestamp = null;
|
||||
const CACHE_TTL = parseInt(process.env.WORKFLOW_CACHE_TTL) || 3600000; // 1 hour in milliseconds
|
||||
|
||||
// Clear cache when profile changes — workflows are per-tenant, so the previous
|
||||
@@ -17,43 +22,52 @@ const CACHE_TTL = parseInt(process.env.WORKFLOW_CACHE_TTL) || 3600000; // 1 hour
|
||||
profileManager.onSwitch(() => clearCache());
|
||||
|
||||
/**
|
||||
* Check if cache is still valid
|
||||
* Application effective : celle demandée, sinon celle du profil actif.
|
||||
*/
|
||||
function isCacheValid() {
|
||||
if (!workflowCache || !cacheTimestamp) {
|
||||
function resolveApplication(application) {
|
||||
return (application && application.trim()) || profileManager.getCurrent().application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache is still valid for an application
|
||||
*/
|
||||
function isCacheValid(application) {
|
||||
if (!workflowCaches[application] || !cacheTimestamps[application]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const age = now - cacheTimestamp;
|
||||
const age = Date.now() - cacheTimestamps[application];
|
||||
return age < CACHE_TTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all workflows from API with pagination
|
||||
* Uses high page size (5000) to minimize API calls
|
||||
* Fetch all workflows of an application from API with pagination.
|
||||
* Uses high page size (5000) to minimize API calls.
|
||||
* Lazy : seule l'application effectivement demandée est chargée (D26) — ne
|
||||
* jamais précharger les 9 applications.
|
||||
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||
*/
|
||||
async function fetchAllWorkflows() {
|
||||
async function fetchAllWorkflows(application) {
|
||||
const app = resolveApplication(application);
|
||||
|
||||
// Check cache validity
|
||||
if (isCacheValid()) {
|
||||
console.error('[Workflow] Using cached data');
|
||||
return workflowCache;
|
||||
if (isCacheValid(app)) {
|
||||
console.error(`[Workflow] Using cached data for "${app}"`);
|
||||
return workflowCaches[app];
|
||||
}
|
||||
|
||||
console.error('[Workflow] Cache expired or empty, fetching from API...');
|
||||
console.error(`[Workflow] Cache expired or empty for "${app}", fetching from API...`);
|
||||
|
||||
try {
|
||||
let allWorkflows = [];
|
||||
let offset = 0;
|
||||
const pageSize = parseInt(process.env.WORKFLOW_PAGE_SIZE) || 5000;
|
||||
const profile = profileManager.getCurrent();
|
||||
const application = profile.application;
|
||||
const tenant = profile.tenant;
|
||||
const tenant = profileManager.getCurrent().tenant;
|
||||
|
||||
while (true) {
|
||||
const body = [application, tenant, pageSize, offset];
|
||||
const body = [app, tenant, pageSize, offset];
|
||||
|
||||
console.error(`[Workflow] Fetching page: offset=${offset}, pageSize=${pageSize}`);
|
||||
console.error(`[Workflow] Fetching page: application=${app}, offset=${offset}, pageSize=${pageSize}`);
|
||||
|
||||
// Use AD API (useAdApi=true)
|
||||
const response = await apiService.post('/Workflow/GetByApplication', body, true);
|
||||
@@ -80,17 +94,45 @@ async function fetchAllWorkflows() {
|
||||
}
|
||||
|
||||
// Update cache
|
||||
workflowCache = allWorkflows;
|
||||
cacheTimestamp = Date.now();
|
||||
workflowCaches[app] = allWorkflows;
|
||||
cacheTimestamps[app] = Date.now();
|
||||
|
||||
console.error(`[Workflow] Successfully cached ${allWorkflows.length} workflows`);
|
||||
console.error(`[Workflow] Successfully cached ${allWorkflows.length} workflows for "${app}"`);
|
||||
return allWorkflows;
|
||||
} catch (error) {
|
||||
console.error('[Workflow] Error fetching workflows:', error.message);
|
||||
throw new Error(`Failed to fetch workflows: ${error.message}`);
|
||||
console.error(`[Workflow] Error fetching workflows for "${app}":`, error.message);
|
||||
throw new Error(`Failed to fetch workflows for application "${app}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les applications déclarées (POST /Application/GetAll, payload null).
|
||||
* La réponse est une enveloppe { entities: [...] } (D4) dont chaque élément
|
||||
* porte un blob `data` volumineux — on ne conserve que les champs légers.
|
||||
* Cache TTL commun, vidé au switch de profil.
|
||||
* @returns {Promise<Array<{name: string, id: string, version: number}>>}
|
||||
*/
|
||||
async function fetchApplications() {
|
||||
if (applicationsCache && applicationsTimestamp &&
|
||||
Date.now() - applicationsTimestamp < CACHE_TTL) {
|
||||
return applicationsCache;
|
||||
}
|
||||
|
||||
console.error('[Workflow] Fetching application list (Application/GetAll)...');
|
||||
const response = await apiService.post('/Application/GetAll', null, true);
|
||||
const entities = response?.entities || [];
|
||||
|
||||
applicationsCache = entities.map(a => ({
|
||||
name: a.name || a.Name,
|
||||
id: a.id || a.Id,
|
||||
version: a.version ?? a.Version,
|
||||
})).filter(a => a.name);
|
||||
applicationsTimestamp = Date.now();
|
||||
|
||||
console.error(`[Workflow] Cached ${applicationsCache.length} application(s)`);
|
||||
return applicationsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search workflows by query string.
|
||||
* Real AD keys (lowercase, cf. D5): id, name, version, applicationName,
|
||||
@@ -99,9 +141,10 @@ async function fetchAllWorkflows() {
|
||||
* @param {string|null} category - Optional applicationName filter (the only
|
||||
* grouping the AD API provides)
|
||||
* @param {number} limit - Maximum results to return
|
||||
* @param {string} [application] - Application AD interrogée (défaut : profil)
|
||||
*/
|
||||
async function searchWorkflows(query, category = null, limit = 50) {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
async function searchWorkflows(query, category = null, limit = 50, application) {
|
||||
const workflows = await fetchAllWorkflows(application);
|
||||
|
||||
let results = workflows;
|
||||
|
||||
@@ -130,15 +173,17 @@ async function searchWorkflows(query, category = null, limit = 50) {
|
||||
/**
|
||||
* Get workflow details by ID
|
||||
* @param {string|number} workflowId - Workflow ID
|
||||
* @param {string} [application] - Application AD interrogée (défaut : profil)
|
||||
*/
|
||||
async function getWorkflowDetails(workflowId) {
|
||||
async function getWorkflowDetails(workflowId, application) {
|
||||
// Garde d'entrée : sans elle, un workflow_id absent matchait le premier
|
||||
// workflow du cache (undefined === undefined sur les clés mortes ci-dessous).
|
||||
if (workflowId == null || workflowId === '') {
|
||||
throw new Error('workflow_id est requis (id ou nom exact du workflow). Utilisez search_workflows pour le trouver.');
|
||||
}
|
||||
|
||||
const workflows = await fetchAllWorkflows();
|
||||
const app = resolveApplication(application);
|
||||
const workflows = await fetchAllWorkflows(app);
|
||||
|
||||
// Clés réelles de l'API AD (minuscules, D5) : id, name. Les variantes
|
||||
// Id/Code/Name n'existent pas sur ces objets — les comparer faisait matcher
|
||||
@@ -149,82 +194,65 @@ async function getWorkflowDetails(workflowId) {
|
||||
);
|
||||
|
||||
if (!workflow) {
|
||||
throw new Error(`Workflow not found: ${workflowId}. Utilisez search_workflows pour trouver l'id ou le nom exact.`);
|
||||
throw new Error(
|
||||
`Workflow not found: ${workflowId} (application "${app}"). ` +
|
||||
`Utilisez search_workflows pour trouver l'id ou le nom exact — ` +
|
||||
`pensez au paramètre application (ex: "CustomApp" pour le spécifique client).`
|
||||
);
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* List distinct applicationName values.
|
||||
* Workflows have no category field — applicationName is the only grouping the
|
||||
* AD API provides, and every workflow of the active application carries the
|
||||
* same value (e.g. "EasyWMS").
|
||||
* Get workflow statistics for one application
|
||||
* @param {string} [application] - Application AD interrogée (défaut : profil)
|
||||
*/
|
||||
async function listWorkflowCategories() {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
|
||||
const categories = new Set();
|
||||
workflows.forEach(w => {
|
||||
const applicationName = w.applicationName || w.ApplicationName;
|
||||
if (applicationName) {
|
||||
categories.add(applicationName);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort alphabetically
|
||||
return Array.from(categories).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow statistics
|
||||
*/
|
||||
async function getWorkflowStats() {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
const categories = await listWorkflowCategories();
|
||||
|
||||
// Count workflows per applicationName (the only grouping in the data)
|
||||
const categoryCounts = {};
|
||||
workflows.forEach(w => {
|
||||
const cat = w.applicationName || w.ApplicationName || '(unknown)';
|
||||
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
||||
});
|
||||
async function getWorkflowStats(application) {
|
||||
const app = resolveApplication(application);
|
||||
const workflows = await fetchAllWorkflows(app);
|
||||
|
||||
return {
|
||||
application: app,
|
||||
total: workflows.length,
|
||||
categories: categories.length,
|
||||
categoryCounts,
|
||||
cacheAge: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null
|
||||
cacheAge: cacheTimestamps[app] ? Math.floor((Date.now() - cacheTimestamps[app]) / 1000) : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear workflow cache (force refresh on next request)
|
||||
* Clear workflow caches (force refresh on next request) — toutes applications.
|
||||
*/
|
||||
function clearCache() {
|
||||
workflowCache = null;
|
||||
cacheTimestamp = null;
|
||||
workflowCaches = {};
|
||||
cacheTimestamps = {};
|
||||
applicationsCache = null;
|
||||
applicationsTimestamp = null;
|
||||
console.error('[Workflow] Cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache status
|
||||
* Get cache status, per application (D26)
|
||||
* @returns {Object} application -> { cached, count, timestamp, age, valid }
|
||||
*/
|
||||
function getCacheStatus() {
|
||||
return {
|
||||
cached: workflowCache !== null,
|
||||
count: workflowCache ? workflowCache.length : 0,
|
||||
timestamp: cacheTimestamp,
|
||||
age: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null,
|
||||
valid: isCacheValid()
|
||||
};
|
||||
const status = {};
|
||||
Object.keys(workflowCaches).forEach(app => {
|
||||
status[app] = {
|
||||
cached: true,
|
||||
count: workflowCaches[app].length,
|
||||
timestamp: cacheTimestamps[app],
|
||||
age: cacheTimestamps[app] ? Math.floor((Date.now() - cacheTimestamps[app]) / 1000) : null,
|
||||
valid: isCacheValid(app)
|
||||
};
|
||||
});
|
||||
return status;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAllWorkflows,
|
||||
fetchApplications,
|
||||
searchWorkflows,
|
||||
getWorkflowDetails,
|
||||
listWorkflowCategories,
|
||||
getWorkflowStats,
|
||||
clearCache,
|
||||
getCacheStatus
|
||||
|
||||
+46
-31
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
const adService = require('../services/ad-service');
|
||||
const workflowService = require('../services/workflow-service');
|
||||
|
||||
/**
|
||||
* List available AD tools
|
||||
@@ -12,7 +13,7 @@ function listTools() {
|
||||
return [
|
||||
{
|
||||
name: 'get_application_summary',
|
||||
description: 'Get summary of Application Dictionary elements. Shows count of cached elements per type (Commands, Queries, Dialogs, Views, etc.). Only counts already-loaded types to avoid long waits.',
|
||||
description: 'Get summary of Application Dictionary caches, grouped by application then element type (D26), plus the per-application workflow caches. Only already-loaded entries are detailed to avoid long waits.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -21,7 +22,7 @@ function listTools() {
|
||||
},
|
||||
{
|
||||
name: 'get_ad_elements',
|
||||
description: 'Get all elements of a specific type from Application Dictionary. Supports: Command, Query, Dialog, View, Entity, Event, Hook, Report, Dashboard, and 11 other types (20 total). Elements are lazy-loaded and cached for 1 hour.',
|
||||
description: 'Get all elements of a specific type from Application Dictionary. Supports: Command, Query, Dialog, View, Entity, Event, Hook, Report, Dashboard, and 11 other types (20 total). Elements are lazy-loaded and cached for 1 hour, per (application, type).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -35,6 +36,10 @@ function listTools() {
|
||||
description: 'Maximum number of elements to return (default: 100, max: 1000)',
|
||||
default: 100,
|
||||
},
|
||||
application: {
|
||||
type: 'string',
|
||||
description: 'AD application to query (default: the active profile\'s application, usually EasyWMS). Client-specific elements live in "CustomApp" (CST_* prefix). Full list via list_workflow_categories.',
|
||||
},
|
||||
},
|
||||
required: ['element_type'],
|
||||
},
|
||||
@@ -59,6 +64,10 @@ function listTools() {
|
||||
description: 'Maximum results (default: 50)',
|
||||
default: 50,
|
||||
},
|
||||
application: {
|
||||
type: 'string',
|
||||
description: 'AD application to search in (default: the active profile\'s application, usually EasyWMS). Client-specific elements live in "CustomApp" (CST_* prefix).',
|
||||
},
|
||||
},
|
||||
required: ['element_type', 'query'],
|
||||
},
|
||||
@@ -78,6 +87,10 @@ function listTools() {
|
||||
type: 'string',
|
||||
description: 'Element ID or name',
|
||||
},
|
||||
application: {
|
||||
type: 'string',
|
||||
description: 'AD application the element belongs to (default: the active profile\'s application, usually EasyWMS). Client-specific elements live in "CustomApp" (CST_* prefix).',
|
||||
},
|
||||
},
|
||||
required: ['element_type', 'element_id'],
|
||||
},
|
||||
@@ -140,38 +153,37 @@ async function executeTool(name, args) {
|
||||
async function getApplicationSummaryTool(args) {
|
||||
console.error('[ADTools] Getting application summary');
|
||||
|
||||
const summary = adService.getApplicationSummary();
|
||||
// État par (application, type) — seules les entrées en cache sont
|
||||
// détaillées, la sortie reste bornée quel que soit le nombre
|
||||
// d'applications interrogées (D24, D26).
|
||||
const adByApplication = adService.getApplicationSummary();
|
||||
const workflowsByApplication = workflowService.getCacheStatus();
|
||||
|
||||
// Calculate totals
|
||||
let totalCached = 0;
|
||||
let cachedEntries = 0;
|
||||
let totalElements = 0;
|
||||
const cachedTypes = [];
|
||||
const uncachedTypes = [];
|
||||
|
||||
Object.entries(summary).forEach(([type, info]) => {
|
||||
if (info.cached) {
|
||||
totalCached++;
|
||||
Object.values(adByApplication).forEach(types => {
|
||||
Object.values(types).forEach(info => {
|
||||
cachedEntries++;
|
||||
totalElements += info.count;
|
||||
cachedTypes.push(type);
|
||||
} else {
|
||||
uncachedTypes.push(type);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const availableTypes = adService.getAvailableTypes();
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
summary: {
|
||||
totalTypes: Object.keys(summary).length,
|
||||
cachedTypes: totalCached,
|
||||
uncachedTypes: uncachedTypes.length,
|
||||
totalElements: totalElements
|
||||
availableTypes: availableTypes.length,
|
||||
cachedEntries,
|
||||
totalElements,
|
||||
applications: Object.keys(adByApplication)
|
||||
},
|
||||
elementCounts: summary,
|
||||
cached: cachedTypes,
|
||||
notCached: uncachedTypes
|
||||
adElementsByApplication: adByApplication,
|
||||
workflowCachesByApplication: workflowsByApplication,
|
||||
note: 'Caches AD par (application, type) et caches workflows par application — chargés paresseusement à la première demande. Types valides via list_ad_types.'
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
@@ -181,11 +193,11 @@ async function getApplicationSummaryTool(args) {
|
||||
* Tool: get_ad_elements
|
||||
*/
|
||||
async function getADElementsTool(args) {
|
||||
const { element_type, limit = 100 } = args;
|
||||
const { element_type, limit = 100, application } = args;
|
||||
|
||||
console.error(`[ADTools] Getting ${element_type} elements (limit: ${limit})`);
|
||||
console.error(`[ADTools] Getting ${element_type} elements (limit: ${limit}, application: ${application || '(profil)'})`);
|
||||
|
||||
const elements = await adService.getElements(element_type);
|
||||
const elements = await adService.getElements(element_type, application);
|
||||
|
||||
// Limit results
|
||||
const limitedElements = elements.slice(0, Math.min(limit, 1000));
|
||||
@@ -208,6 +220,7 @@ async function getADElementsTool(args) {
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
elementType: element_type,
|
||||
...(application ? { application } : {}),
|
||||
count: elements.length,
|
||||
returned: mappedElements.length,
|
||||
elements: mappedElements
|
||||
@@ -220,11 +233,11 @@ async function getADElementsTool(args) {
|
||||
* Tool: search_ad_elements
|
||||
*/
|
||||
async function searchADElementsTool(args) {
|
||||
const { element_type, query, limit = 50 } = args;
|
||||
const { element_type, query, limit = 50, application } = args;
|
||||
|
||||
console.error(`[ADTools] Searching ${element_type}: query="${query}", limit=${limit}`);
|
||||
console.error(`[ADTools] Searching ${element_type}: query="${query}", limit=${limit}, application=${application || '(profil)'}`);
|
||||
|
||||
const results = await adService.searchElements(element_type, query, limit);
|
||||
const results = await adService.searchElements(element_type, query, limit, application);
|
||||
|
||||
// Map to simplified format
|
||||
const mappedResults = results.map(e => ({
|
||||
@@ -240,6 +253,7 @@ async function searchADElementsTool(args) {
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
elementType: element_type,
|
||||
...(application ? { application } : {}),
|
||||
query,
|
||||
count: mappedResults.length,
|
||||
elements: mappedResults
|
||||
@@ -252,11 +266,11 @@ async function searchADElementsTool(args) {
|
||||
* Tool: get_ad_element_details
|
||||
*/
|
||||
async function getADElementDetailsTool(args) {
|
||||
const { element_type, element_id } = args;
|
||||
const { element_type, element_id, application } = args;
|
||||
|
||||
console.error(`[ADTools] Getting ${element_type} details: ${element_id}`);
|
||||
console.error(`[ADTools] Getting ${element_type} details: ${element_id} (application: ${application || '(profil)'})`);
|
||||
|
||||
const element = await adService.getElementDetails(element_type, element_id);
|
||||
const element = await adService.getElementDetails(element_type, element_id, application);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
@@ -264,6 +278,7 @@ async function getADElementDetailsTool(args) {
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
elementType: element_type,
|
||||
...(application ? { application } : {}),
|
||||
element
|
||||
}, null, 2)
|
||||
}]
|
||||
|
||||
+49
-20
@@ -12,7 +12,7 @@ function listTools() {
|
||||
return [
|
||||
{
|
||||
name: 'search_workflows',
|
||||
description: 'Search workflows by name, description, or code. Returns matching workflows with metadata. Workflows are lazy-loaded from API on first request and cached for 1 hour.',
|
||||
description: 'Search workflows by name. Returns matching workflows with metadata. Workflows are lazy-loaded from API on first request and cached for 1 hour, per application. Client-specific workflows (CST_* prefix) live in the "CustomApp" application — pass application: "CustomApp" to search them.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -23,26 +23,34 @@ function listTools() {
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Filter by applicationName — the only grouping the AD API provides (workflows have no category field). All workflows of the active application share the same value (e.g. "EasyWMS").',
|
||||
description: 'Filter by the applicationName field of the returned workflows (workflows have no category field). Since `application` selects which application is fetched, all its workflows share the same applicationName — prefer `application` to change scope; `category` only narrows within the fetched set.',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum results to return (default: 50)',
|
||||
default: 50,
|
||||
},
|
||||
application: {
|
||||
type: 'string',
|
||||
description: 'AD application whose workflows are searched (default: the active profile\'s application, usually EasyWMS). Client-specific workflows live in "CustomApp". Full list via list_workflow_categories.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_workflow_details',
|
||||
description: 'Get full details of a specific workflow by ID or code',
|
||||
description: 'Get full details of a specific workflow by ID or name',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
workflow_id: {
|
||||
type: 'string',
|
||||
description: 'Workflow ID or code',
|
||||
description: 'Workflow ID or exact name',
|
||||
},
|
||||
application: {
|
||||
type: 'string',
|
||||
description: 'AD application the workflow belongs to (default: the active profile\'s application, usually EasyWMS). Client-specific workflows (CST_*) live in "CustomApp".',
|
||||
},
|
||||
},
|
||||
required: ['workflow_id'],
|
||||
@@ -50,11 +58,16 @@ function listTools() {
|
||||
},
|
||||
{
|
||||
name: 'list_workflow_categories',
|
||||
description: 'List workflow groupings by applicationName. Workflows have no category field in the AD API — applicationName is the only grouping available, and all workflows of the active application share the same value.',
|
||||
description: 'List the AD applications declared on the tenant (Application/GetAll) with their workflow counts where already loaded. Workflows have no category field — the application is the only grouping. Use the `application` parameter of the workflow/AD tools to query a specific one (e.g. "CustomApp" for client-specific CST_* workflows).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
properties: {
|
||||
application: {
|
||||
type: 'string',
|
||||
description: 'Load and count the workflows of this application (default: the active profile\'s application). Other applications are listed without loading them.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -98,17 +111,18 @@ async function executeTool(name, args) {
|
||||
* Tool: search_workflows
|
||||
*/
|
||||
async function searchWorkflows(args) {
|
||||
const { query, category, limit = 50 } = args;
|
||||
const { query, category, limit = 50, application } = args;
|
||||
|
||||
console.error(`[WorkflowTools] Searching workflows: query="${query}", category="${category}", limit=${limit}`);
|
||||
console.error(`[WorkflowTools] Searching workflows: query="${query}", category="${category}", limit=${limit}, application=${application || '(profil)'}`);
|
||||
|
||||
const results = await workflowService.searchWorkflows(query, category, limit);
|
||||
const results = await workflowService.searchWorkflows(query, category, limit, application);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
...(application ? { application } : {}),
|
||||
count: results.length,
|
||||
// Clés réelles de l'API AD (minuscules, cf. D5) : id, name, version,
|
||||
// applicationName, commonInfo. Pas de code/category/description.
|
||||
@@ -133,11 +147,11 @@ async function searchWorkflows(args) {
|
||||
* Tool: get_workflow_details
|
||||
*/
|
||||
async function getWorkflowDetails(args) {
|
||||
const { workflow_id } = args;
|
||||
const { workflow_id, application } = args;
|
||||
|
||||
console.error(`[WorkflowTools] Getting workflow details: ${workflow_id}`);
|
||||
console.error(`[WorkflowTools] Getting workflow details: ${workflow_id} (application: ${application || '(profil)'})`);
|
||||
|
||||
const workflow = await workflowService.getWorkflowDetails(workflow_id);
|
||||
const workflow = await workflowService.getWorkflowDetails(workflow_id, application);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
@@ -154,22 +168,37 @@ async function getWorkflowDetails(args) {
|
||||
* Tool: list_workflow_categories
|
||||
*/
|
||||
async function listWorkflowCategories(args) {
|
||||
console.error('[WorkflowTools] Listing workflow categories');
|
||||
const { application } = args || {};
|
||||
|
||||
const categories = await workflowService.listWorkflowCategories();
|
||||
const stats = await workflowService.getWorkflowStats();
|
||||
console.error(`[WorkflowTools] Listing applications (workflow groupings), application=${application || '(profil)'}`);
|
||||
|
||||
// La liste vient d'Application/GetAll (9 applications sur le tenant mesuré),
|
||||
// pas des applicationName du seul cache actif (D26). Seule l'application
|
||||
// demandée (ou celle du profil) est chargée — pas de préchargement des
|
||||
// autres (D10) : leurs comptes n'apparaissent que si déjà en cache.
|
||||
const applications = await workflowService.fetchApplications();
|
||||
const stats = await workflowService.getWorkflowStats(application);
|
||||
const cacheStatus = workflowService.getCacheStatus();
|
||||
|
||||
const enriched = applications.map(a => ({
|
||||
name: a.name,
|
||||
version: a.version,
|
||||
...(cacheStatus[a.name]
|
||||
? { workflowCount: cacheStatus[a.name].count, cacheAge: cacheStatus[a.name].age }
|
||||
: { workflowCount: null }),
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
note: 'Workflows have no category field in the AD API — these are the distinct applicationName values, the only grouping available. All workflows of the active application share the same value.',
|
||||
totalCategories: categories.length,
|
||||
categories,
|
||||
stats: {
|
||||
note: 'Workflows have no category field in the AD API — the application is the only grouping. workflowCount is only known for applications already loaded (lazy loading); pass application to search_workflows/get_ad_elements to load one.',
|
||||
totalApplications: applications.length,
|
||||
applications: enriched,
|
||||
loaded: {
|
||||
application: stats.application,
|
||||
totalWorkflows: stats.total,
|
||||
categoryCounts: stats.categoryCounts,
|
||||
cacheAge: stats.cacheAge
|
||||
}
|
||||
}, null, 2)
|
||||
|
||||
Reference in New Issue
Block a user