Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90b2c89ff1 | |||
| b941f367ae | |||
| 92de85cf53 | |||
| 706e628715 | |||
| 88dab289cc | |||
| cc34894ae4 | |||
| 35b53dbef5 |
@@ -170,13 +170,18 @@ disponibles : c'est ainsi que Claude sait appeler `switch_wms_profile`.
|
|||||||
|
|
||||||
## Caches
|
## Caches
|
||||||
|
|
||||||
Deux caches, TTL commun `WORKFLOW_CACHE_TTL` (3 600 000 ms), chargement
|
TTL commun `WORKFLOW_CACHE_TTL` (3 600 000 ms), chargement paresseux, vidés à
|
||||||
paresseux, vidés à chaque bascule de profil (D10).
|
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 |
|
| Cache | Granularité | Pagination |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `workflow-service` | global (~3 700 workflows) | `WORKFLOW_PAGE_SIZE`, 5000 |
|
| `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 type** (20 types) | `AD_ELEMENT_TYPES` : `View` 200, `Workflow` 5000, `Resource` 15000, autres 100000 |
|
| `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 —
|
Les tailles de page par type viennent de l'observation des timeouts serveur —
|
||||||
ne les augmentez pas à l'aveugle.
|
ne les augmentez pas à l'aveugle.
|
||||||
@@ -205,8 +210,10 @@ la plus fréquente :
|
|||||||
|
|
||||||
Autres règles :
|
Autres règles :
|
||||||
|
|
||||||
- **`QueryType: 0` (Reading)**, jamais 1 : les statuts sont alors des chaînes
|
- **`QueryType: 0` (Reading) par défaut** : les statuts sont alors des chaînes
|
||||||
(D3).
|
(D3). La bascule vers Writing/Metrics passe par le paramètre `query_type`
|
||||||
|
des outils de requête — un opt-in documenté (D25), jamais un défaut : ne
|
||||||
|
recopiez aucun exemple en `QueryType: 1`.
|
||||||
- **Pas de date relative.** `DateTime.Now`, `DateTime.Today`, `AddDays()` ne
|
- **Pas de date relative.** `DateTime.Now`, `DateTime.Today`, `AddDays()` ne
|
||||||
sont pas traduisibles : écrire `new DateTime(2026, 8, 1)` (D12).
|
sont pas traduisibles : écrire `new DateTime(2026, 8, 1)` (D12).
|
||||||
- **`select_expression` est instable** : les projections via le paramètre
|
- **`select_expression` est instable** : les projections via le paramètre
|
||||||
@@ -233,11 +240,13 @@ pluriel), `Tasks`, `Stocks`, `ProductLocations`, `InboundOrders`, `Receptions`,
|
|||||||
confondues) s'obtient par `get_entity_metadata` (API Metadata) — le catalogue
|
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.
|
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
|
**Application Dictionary** : 20 types, ~38 800 éléments (sur `EasyWMS`).
|
||||||
de loin le plus lourd ; 3 types sont valides mais vides (`Dashboard`,
|
`Resource` (29 374) est de loin le plus lourd ; 3 types sont valides mais vides
|
||||||
`TimelineTemplate`, `Toggle`). `WorkflowAction` et `WritingModel` ont été
|
(`Dashboard`, `TimelineTemplate`, `Toggle`). `WorkflowAction` et `WritingModel`
|
||||||
retirés — 404 (D17). Détail :
|
ont été retirés — 404 (D17). Détail :
|
||||||
[docs/ad-api-validation.md](docs/ad-api-validation.md).
|
[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
|
**Paramètres système** : pas d'entité `CommandParameterData`. La configuration
|
||||||
se lit dans `Parameter` (+ `DefaultValue`) et `ParamValue` (surcharges par
|
se lit dans `Parameter` (+ `DefaultValue`) et `ParamValue` (surcharges par
|
||||||
|
|||||||
@@ -480,3 +480,91 @@ Deux garde-fous de cadrage :
|
|||||||
Au passage, `totalParameters` a changé de sens : c'était le nombre brut
|
Au passage, `totalParameters` a changé de sens : c'était le nombre brut
|
||||||
d'entités `Parameter` chargées, c'est désormais le total correspondant aux
|
d'entités `Parameter` chargées, c'est désormais le total correspondant aux
|
||||||
filtres avant pagination (identique sans filtre).
|
filtres avant pagination (identique sans filtre).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D25 — `query_type` : opt-in explicite, D3 reste la règle par défaut
|
||||||
|
|
||||||
|
**Contexte (mesures des 24-25/08/2026, `LIMAGRAI2512`).** `QueryType` était
|
||||||
|
figé à `0` en dur dans `executeQuery()` et `executeScalarQuery()`, rendant le
|
||||||
|
modèle Writing — opérationnel et mesuré (`Context.Products` en `QueryType: 1`
|
||||||
|
répond) — inatteignable.
|
||||||
|
|
||||||
|
**Décision.** Un paramètre `query_type` (entier, défaut `0`) est exposé sur
|
||||||
|
**trois outils** : `call_query_api`, `query_wms_entities`,
|
||||||
|
`count_wms_entities`. `get_entity_schema` et `search_wms_data` restent des
|
||||||
|
raccourcis Reading, sans paramètre.
|
||||||
|
|
||||||
|
**Rapport à D3.** D3 n'est pas révisée : le Reading reste la règle par défaut,
|
||||||
|
car en Writing les statuts sont des **énumérations** — les comparaisons de
|
||||||
|
chaînes (`== "Release"`), cas le plus courant en debug, y échouent. La bascule
|
||||||
|
est un opt-in explicite et les descriptions d'outils portent l'avertissement.
|
||||||
|
|
||||||
|
Modalités :
|
||||||
|
|
||||||
|
- **Garde de valeur dans le code de l'outil**, pas dans le wrapper : D23 valide
|
||||||
|
les noms de paramètres, pas les valeurs. Hors `0..3` (ou non entier) →
|
||||||
|
erreur locale via `assertValidQueryType()` (`wms-query-service.js`), **avant
|
||||||
|
tout appel réseau**, nommant les quatre contextes.
|
||||||
|
- **`2` et `3` sont transmis tels quels** : le WMS répond et son diagnostic
|
||||||
|
remonte entier (L1.1). Sur `LIMAGRAI2512` : `2` = DataWarehouse non configuré
|
||||||
|
(`Could not resolve serviceType 'IDataWarehouse…'`), `3` = Metrics, contexte
|
||||||
|
présent mais modèle distinct (`'ApplicationMetricDataContext' ne contient pas
|
||||||
|
de définition pour 'Products'`).
|
||||||
|
- **Interaction avec le resolver (D21)** : la table de résolution est
|
||||||
|
construite sur le Metadata **Reading**. Quand `query_type != 0`, un nom qui
|
||||||
|
se résout se résout normalement (`Products` marche en Writing, mesuré) ; un
|
||||||
|
nom **inconnu** du Reading n'est **pas** bloqué — il passe tel quel avec un
|
||||||
|
`warning` dans la réponse (`allowUnknown` du resolver, même mécanique que le
|
||||||
|
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.
|
||||||
|
|||||||
+41
-70
@@ -17,77 +17,13 @@ contient que ce qui reste à faire.
|
|||||||
Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les
|
Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les
|
||||||
chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`.
|
chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`.
|
||||||
|
|
||||||
### L4.1 — Le modèle Writing est inatteignable
|
### L4.1 (reliquat) — Explorer le contexte Metrics
|
||||||
|
|
||||||
`QueryType` est figé à `0` (Reading) en dur dans `api-service.js`
|
L'exposition de `query_type` est livrée (D25). Reste l'investigation : le
|
||||||
(`executeQuery` et `executeScalarQuery`). Or `QueryContextType` a **quatre**
|
contexte `Metrics` (`QueryType: 3`, `ApplicationMetricDataContext`) mérite une
|
||||||
valeurs. Testées une à une :
|
exploration à part — c'est probablement là que vivent les données agrégées
|
||||||
|
produites par les jobs `MetricGatherer`. Livrable : un rapport, pas du code
|
||||||
| Valeur | Contexte | Résultat sur `LIMAGRAI2512` |
|
(même phase d'investigation que L4.4).
|
||||||
|---|---|---|
|
|
||||||
| `0` | Reading | opérationnel (seul utilisé aujourd'hui) |
|
|
||||||
| `1` | Writing | **opérationnel** — `Context.Products` répond |
|
|
||||||
| `2` | DataWarehouse | **non configuré** : `Could not resolve serviceType 'IDataWarehouse…'` |
|
|
||||||
| `3` | Metrics | contexte présent (`ApplicationMetricDataContext`), modèle non exploré |
|
|
||||||
|
|
||||||
Exposer `query_type` sur les outils de requête, défaut `0`. Attention : D3 reste
|
|
||||||
vrai — en Writing les champs de statut sont des **énumérations**, donc
|
|
||||||
`== "Release"` échoue. La bascule doit être un choix explicite et documenté.
|
|
||||||
|
|
||||||
Le contexte `Metrics` mérite une exploration à part : c'est probablement là que
|
|
||||||
vivent les données agrégées produites par les jobs `MetricGatherer`.
|
|
||||||
|
|
||||||
### 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
|
### L4.3 — Identifier le MCP dans les logs du WMS
|
||||||
|
|
||||||
@@ -148,6 +84,41 @@ plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Lot 5 — Clore la famille D24 (rejets client sur sorties volumineuses)
|
||||||
|
|
||||||
|
Mesures du 25/08/2026 (protocole, LIMAGRAIN), toutes au-dessus du seuil de
|
||||||
|
rejet client (~70 000 caractères, D24) :
|
||||||
|
|
||||||
|
| Appel | Taille |
|
||||||
|
|---|---:|
|
||||||
|
| `query_wms_entities("Products", limit: 200)` — Reading ordinaire | **957 234** |
|
||||||
|
| `search_wms_data("PAL")` | **847 543** |
|
||||||
|
| `get_workflow_details(CST_SendRejectContainersToPK)` | ~101 800 |
|
||||||
|
| `call_query_api("Products", query_type: 1, limit: 1)` — **une seule ligne** Writing | **95 288** |
|
||||||
|
|
||||||
|
### L5.1 — Paginer le blob `data` de `get_workflow_details`
|
||||||
|
|
||||||
|
La définition complète d'un workflow dépasse le seuil (~101 800 pour
|
||||||
|
`CST_SendRejectContainersToPK`, 79 092 pour un `StackerCrane_…` EasyWMS).
|
||||||
|
Découper le blob `data` en tranches verbatim (paramètres de fenêtre au schéma),
|
||||||
|
signalées D24 — sans jamais résumer ni reformuler le contenu.
|
||||||
|
|
||||||
|
### L5.2 — Garde de taille sur les outils de requête
|
||||||
|
|
||||||
|
Une ligne Writing = un agrégat complet sérialisé (95 288 caractères là où la
|
||||||
|
même ligne Reading en fait ~4 500) ; 200 lignes Reading = ~957 000 ; `search_wms_data`
|
||||||
|
= ~848 000. Garde de taille commune sur `query_wms_entities`, `call_query_api`
|
||||||
|
et `search_wms_data` : lignes entières écartées, `truncated`/`returned`/`omitted`/`hint`
|
||||||
|
(D24). Ne pas réduire `MAX_QUERY_ROWS` ni les limites par défaut.
|
||||||
|
|
||||||
|
### L5.3 — Champ `tool` absent des erreurs construites localement
|
||||||
|
|
||||||
|
Les `catch` locaux d'`api-tools.js` renvoient `{ success: false, error }` sans
|
||||||
|
le champ `tool` du contrat (convention 3) — antérieur au lot 4. Balayer tous
|
||||||
|
les modules d'outils pour le même motif.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Écarté
|
## Écarté
|
||||||
|
|
||||||
| Proposition | Raison |
|
| Proposition | Raison |
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Passation — lot 5 (clore la famille D24)
|
||||||
|
|
||||||
|
Tu travailles sur `mcp-wms-api` : un serveur MCP (Node.js, CommonJS, stdio) qui
|
||||||
|
donne à Claude un accès en lecture à un WMS EasyWMS (Mecalux) via ses API REST.
|
||||||
|
Lis [../CLAUDE.md](../CLAUDE.md) et [../DECISIONS.md](../DECISIONS.md) —
|
||||||
|
en particulier **D24**, le contrat de troncature que ce lot généralise — avant
|
||||||
|
de toucher au code.
|
||||||
|
|
||||||
|
**Mission** : éliminer les derniers cas connus de réponses d'outils dépassant
|
||||||
|
le seuil de rejet des clients MCP (~70 000 caractères, D24) — lot 5 de
|
||||||
|
[../ROADMAP.md](../ROADMAP.md) : pagination du blob `data` de
|
||||||
|
`get_workflow_details` (L5.1), garde de taille sur les trois outils de requête
|
||||||
|
(L5.2), champ `tool` manquant dans les enveloppes d'erreur locales (L5.3).
|
||||||
|
|
||||||
|
**Hors périmètre** : tout le reste de la roadmap (L4.3, L4.4, L4.5,
|
||||||
|
exploration Metrics). **`QueryExecuteStream` est explicitement interdit** —
|
||||||
|
piste long terme consignée en L4.5, pas ce lot. Aucun nouvel outil : le compte
|
||||||
|
reste à 23. Ne pousse rien (`git push` interdit), ne touche pas au `.env`,
|
||||||
|
n'appelle jamais `execute_command` (il écrit dans le WMS).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexte matériel
|
||||||
|
|
||||||
|
- Profil de travail : `LIMAGRAIN` (par défaut), host `10.255.255.2`, tenant
|
||||||
|
`LIMAGRAI2512`.
|
||||||
|
- **Le profil `AD` est cassé et c'est diagnostiqué — ne le réinvestigue pas**
|
||||||
|
(tenant introuvable côté STS, point ouvert de la ROADMAP). La baseline se
|
||||||
|
mesure avec `npm test` (profil par défaut), attendu **4/4, code de sortie 0**.
|
||||||
|
- Baseline protocolaire à préserver : **23 outils**, **6 resources**, aucune
|
||||||
|
écriture sur stdout hors JSON-RPC.
|
||||||
|
|
||||||
|
Handshake + comptages :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%s\n%s\n%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' | node src/index.js 2>/dev/null | node -e "let b='';process.stdin.on('data',d=>b+=d).on('end',()=>{for(const l of b.split('\n').filter(Boolean)){const m=JSON.parse(l);if(m.id===2)console.log('tools:',m.result.tools.length);if(m.id===3)console.log('resources:',m.result.resources.length);}});"
|
||||||
|
```
|
||||||
|
|
||||||
|
Mesurer la taille d'une réponse d'outil (la mesure qui fait foi est la
|
||||||
|
longueur de `content[0].text` via le protocole) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%s\n%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"NOM","arguments":{}}}' | node src/index.js 2>/dev/null | node -e "let b='';process.stdin.on('data',d=>b+=d).on('end',()=>{for(const l of b.split('\n').filter(Boolean)){const m=JSON.parse(l);if(m.id===2)console.log('chars:',m.result.content[0].text.length);}});"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contraintes non négociables
|
||||||
|
|
||||||
|
1. `console.error()` uniquement (D6).
|
||||||
|
2. Contrat d'erreur : `{ success: false, error, tool }`, `isError: true` —
|
||||||
|
c'est précisément l'objet de L5.3.
|
||||||
|
3. Messages et hints actionnables.
|
||||||
|
4. Aucun accès base de données (D1).
|
||||||
|
5. **D23** : le wrapper valide les **noms** de paramètres et les requis contre
|
||||||
|
les schémas — tout paramètre ajouté (fenêtre de L5.1…) doit être déclaré
|
||||||
|
dans l'`inputSchema`. Le wrapper ne valide pas les **valeurs** : les gardes
|
||||||
|
de valeur vivent dans le code de l'outil.
|
||||||
|
6. **D24** : signal commun — `truncated: true` **uniquement** quand la réponse
|
||||||
|
est coupée, `hint` actionnable, `returned` vs total avant coupe. `omitted`
|
||||||
|
en plus quand des éléments entiers sont écartés. Réutilise ce vocabulaire
|
||||||
|
exactement ; ne crée pas un second dialecte.
|
||||||
|
7. **Pas de nouveau numéro de décision attendu** : ce lot **étend D24**
|
||||||
|
(complète son texte : les outils de requête et `get_workflow_details`
|
||||||
|
entrent dans son périmètre). Si une décision réellement nouvelle s'impose,
|
||||||
|
vérifie le dernier numéro (D26 à ce jour) et réserve D27 dans le commit qui
|
||||||
|
l'acte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Confirmer les mesures
|
||||||
|
|
||||||
|
Toutes rejouées le 25/08/2026 (protocole, LIMAGRAIN) avec la commande de
|
||||||
|
mesure ci-dessus. À **confirmer**, pas à réinvestiguer :
|
||||||
|
|
||||||
|
| Appel | Taille constatée |
|
||||||
|
|---|---:|
|
||||||
|
| `query_wms_entities` `{"entity_type":"Products","limit":200}` (Reading) | **957 234** |
|
||||||
|
| `search_wms_data` `{"keyword":"PAL"}` | **847 543** |
|
||||||
|
| `call_query_api` `{"entity_type":"Products","query_type":1,"limit":1}` — une seule ligne Writing | **95 288** |
|
||||||
|
| `get_workflow_details` sur `CST_SendRejectContainersToPK` (`application: "CustomApp"`) | ~101 800 |
|
||||||
|
| `get_workflow_details` `{"workflow_id":"2a320000-0642-47df-aa52-3b89c27c016f"}` (StackerCrane, EasyWMS) | 79 092 (dont blob `data` : 71 512) |
|
||||||
|
|
||||||
|
Attention aux noms de paramètres (D23 les fait respecter) : `search_workflows`
|
||||||
|
prend `query`, `search_wms_data` et `search_logs` prennent `keyword`. Pour
|
||||||
|
trouver l'id du workflow CST :
|
||||||
|
`search_workflows {"query":"CST_SendRejectContainersToPK","application":"CustomApp"}`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L5.1 — Paginer le blob `data` de `get_workflow_details`
|
||||||
|
|
||||||
|
**Problème.** La réponse embarque la définition complète du workflow (blob
|
||||||
|
`data`, XML/JSON EasyBuilder) : 71 512 caractères sur le StackerCrane mesuré,
|
||||||
|
davantage sur les gros `CST_*` — la réponse dépasse le seuil client.
|
||||||
|
|
||||||
|
**À faire.**
|
||||||
|
- Deux paramètres de fenêtre sur le blob `data` (au schéma, D23) : une taille
|
||||||
|
max de tranche (défaut de l'ordre de **20 000** caractères, cohérent avec
|
||||||
|
D24) et un offset (défaut 0). Nommage à ta main (`max_data_chars` /
|
||||||
|
`data_offset` ou équivalent), documenté dans les descriptions.
|
||||||
|
- La réponse porte toujours la taille **totale** du blob ; quand la fenêtre
|
||||||
|
tronque : `truncated: true` + `hint` donnant l'offset suivant.
|
||||||
|
- La tranche est **verbatim** : découpe de chaîne, rien d'autre.
|
||||||
|
- Les métadonnées du workflow (`id`, `name`, `commonInfo`…) restent complètes
|
||||||
|
dans chaque réponse ; seule `data` est fenêtrée.
|
||||||
|
|
||||||
|
**Pente naturelle interdite** : ne résume pas, ne reformule pas, ne « parse »
|
||||||
|
pas le blob pour n'en renvoyer que des morceaux jugés utiles — la définition
|
||||||
|
EasyBuilder doit rester reconstituable à l'octet près en concaténant les
|
||||||
|
tranches.
|
||||||
|
|
||||||
|
**Vérification attendue** (protocole, LIMAGRAIN) :
|
||||||
|
- `get_workflow_details` sur le StackerCrane (`2a320000-0642-47df-aa52-3b89c27c016f`)
|
||||||
|
sans paramètre de fenêtre → réponse < ~30 000 caractères, `truncated: true`,
|
||||||
|
taille totale annoncée = 71 512, hint avec l'offset suivant.
|
||||||
|
- En enchaînant les tranches (offset 0, 20 000, 40 000, 60 000) : la somme des
|
||||||
|
longueurs des tranches = 71 512, et la concaténation est identique au blob
|
||||||
|
d'origine (compare au moins les longueurs et les 100 premiers/derniers
|
||||||
|
caractères).
|
||||||
|
- Un workflow à petit blob (< défaut) → réponse strictement inchangée, pas de
|
||||||
|
`truncated`.
|
||||||
|
|
||||||
|
## L5.2 — Garde de taille sur les outils de requête
|
||||||
|
|
||||||
|
**Problème.** Aucune borne de volume sur `query_wms_entities`, `call_query_api`
|
||||||
|
et `search_wms_data` : 957 234 caractères pour 200 lignes Reading, 847 543
|
||||||
|
pour une recherche, et **95 288 pour une seule ligne Writing** (le modèle
|
||||||
|
Writing sérialise l'agrégat complet — navigations, `$id`… — là où la même
|
||||||
|
ligne Reading fait ~4 500).
|
||||||
|
|
||||||
|
**À faire.**
|
||||||
|
- Garde de taille commune aux trois outils : plafond en variable
|
||||||
|
d'environnement avec défaut (style `MAX_LOG_SEARCH_CHARS`, 25 000 — même
|
||||||
|
ordre de grandeur, nom à ta main, documenté dans CLAUDE.md).
|
||||||
|
- Au-delà du plafond : écarter des **lignes entières** (pour `search_wms_data` :
|
||||||
|
des résultats entiers, par entité), et signaler D24 : `truncated: true`,
|
||||||
|
`returned`, `omitted`, `hint` (réduire `limit`, ajouter un `filter` ; et pour
|
||||||
|
`query_type != 0` : rappeler que les lignes Writing sont des agrégats
|
||||||
|
complets et suggérer Reading si l'usage le permet).
|
||||||
|
- **Cas limite à traiter explicitement** : une seule ligne dépasse le plafond
|
||||||
|
(réel en Writing). La réponse est alors `returned: 0`, `omitted: <n>`,
|
||||||
|
`truncated: true`, avec un hint qui explique pourquoi et quoi faire — c'est
|
||||||
|
moins bon qu'un résultat, mais c'est mieux qu'un rejet client opaque.
|
||||||
|
- **Ne change ni `MAX_QUERY_ROWS`, ni les limites par défaut des outils, ni le
|
||||||
|
comportement sous le plafond** : une requête qui tient aujourd'hui doit
|
||||||
|
renvoyer exactement la même réponse.
|
||||||
|
- L'avertissement de volume mérite une phrase dans la description du paramètre
|
||||||
|
`query_type` (D25).
|
||||||
|
|
||||||
|
**Vérification attendue** (protocole, LIMAGRAIN) :
|
||||||
|
- `query_wms_entities` `{"entity_type":"Products","limit":200}` → réponse sous
|
||||||
|
le plafond (+ marge d'enveloppe), `truncated: true`, `returned` < 200,
|
||||||
|
`omitted` cohérent, hint présent.
|
||||||
|
- `search_wms_data` `{"keyword":"PAL"}` → borné et signalé de même.
|
||||||
|
- `call_query_api` `{"entity_type":"Products","query_type":1,"limit":1}` →
|
||||||
|
`returned: 0`, `omitted: 1`, `truncated: true`, hint expliquant le volume
|
||||||
|
Writing.
|
||||||
|
- `query_wms_entities` `{"entity_type":"Container","limit":1}` → réponse
|
||||||
|
**strictement identique** à aujourd'hui (~4 500 caractères, pas de
|
||||||
|
`truncated`).
|
||||||
|
- `count_wms_entities` → non concerné, inchangé.
|
||||||
|
|
||||||
|
## L5.3 — Champ `tool` dans les enveloppes d'erreur locales
|
||||||
|
|
||||||
|
**Problème.** Les `catch` locaux d'`api-tools.js` (deux blocs, vers les lignes
|
||||||
|
145-162 et 191-208) renvoient `{ success: false, error }` **sans** le champ
|
||||||
|
`tool` du contrat. Preuve : `call_query_api` `{"entity_type":"Products","query_type":7}`
|
||||||
|
répond aujourd'hui `{"success": false, "error": "query_type invalide : …"}` —
|
||||||
|
pas de `tool`.
|
||||||
|
|
||||||
|
**À faire.** Balayer **tous** les modules de `src/tools/` à la recherche
|
||||||
|
d'enveloppes `success: false` construites localement : soit y ajouter `tool`,
|
||||||
|
soit laisser l'erreur remonter au wrapper de `src/index.js` (qui l'ajoute) —
|
||||||
|
au choix selon le cas, mais le résultat observable est uniforme. Attention à ne
|
||||||
|
pas perdre les champs additionnels utiles des enveloppes locales (le `warning`
|
||||||
|
de résolution d'`api-tools`, les listes de profils de `profile-tools`…).
|
||||||
|
|
||||||
|
**Vérification attendue** : `call_query_api` avec `query_type: 7` → l'enveloppe
|
||||||
|
contient `"tool": "call_query_api"` ; un `grep` sur `src/tools/` ne montre plus
|
||||||
|
d'enveloppe `success: false` sans `tool` (colle le résultat du grep dans le
|
||||||
|
compte-rendu).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Méthode
|
||||||
|
|
||||||
|
1. Phase 0 d'abord (cinq mesures, rejouées telles quelles).
|
||||||
|
2. L5.1, puis L5.2, puis L5.3 — chaque bloc vérifié **en exécution via le
|
||||||
|
protocole** avant de passer au suivant.
|
||||||
|
3. Rappel : le serveur traite les `tools/call` en concurrence (point ouvert de
|
||||||
|
la ROADMAP) — pour les vérifications qui comparent des réponses successives,
|
||||||
|
envoie les requêtes séquentiellement.
|
||||||
|
4. Les schémas changent : reboucler sur les 23 noms de `tools/list` (aucun
|
||||||
|
`Unknown tool` ; `execute_command` vérifié statiquement, non appelé) et
|
||||||
|
vérifier qu'un paramètre inconnu est toujours rejeté (D23).
|
||||||
|
5. Baseline avant/après : handshake (23/6) + `npm test` (4/4, exit 0).
|
||||||
|
6. « Non résolu » est une réponse acceptable pour une investigation
|
||||||
|
time-boxée ; une hypothèse présentée comme solution ne l'est pas.
|
||||||
|
|
||||||
|
## Livraison
|
||||||
|
|
||||||
|
- Un commit par bloc (L5.1, L5.2, L5.3), messages expliquant le pourquoi,
|
||||||
|
**mesures avant/après dans le corps du message** (tailles en caractères).
|
||||||
|
- Documentation dans les mêmes commits : **compléter D24** (périmètre étendu
|
||||||
|
aux outils de requête et à `get_workflow_details`) ; CLAUDE.md — la nouvelle
|
||||||
|
variable d'environnement dans les réglages partagés, la fenêtre de
|
||||||
|
`get_workflow_details` si elle change l'usage documenté ; ROADMAP.md —
|
||||||
|
retirer le lot 5.
|
||||||
|
- Ne pousse pas. `.env` intact. Toute anomalie hors périmètre découverte en
|
||||||
|
route : dans ROADMAP.md, pas dans le code.
|
||||||
|
- Compte-rendu final : pour chaque bloc, la vérification attendue rejouée et
|
||||||
|
son résultat **mesuré** (colle les tailles et les sorties), plus la baseline
|
||||||
|
finale. Laisse `handoff-lot5.md` en place pour la révision.
|
||||||
+63
-98
@@ -42,9 +42,13 @@ function getAPICatalog() {
|
|||||||
|
|
||||||
The WMS provides several REST APIs for querying and modifying data.
|
The WMS provides several REST APIs for querying and modifying data.
|
||||||
|
|
||||||
**Base URL:** \`${process.env.WMS_API_BASE_URL || 'https://10.255.255.2/ApplicationService/api'}\`
|
**Base URL:** \`https://<host>/ApplicationService/api\` — built from the active
|
||||||
|
profile's host (see \`get_current_wms_profile\`).
|
||||||
**Authentication:** OAuth 2.0 Bearer Token (automatic)
|
**Authentication:** OAuth 2.0 Bearer Token (automatic)
|
||||||
|
|
||||||
|
The generated help page at \`https://<host>/ApplicationService/Help\` is the
|
||||||
|
authoritative reference for endpoints and fields.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Query API
|
## Query API
|
||||||
@@ -60,44 +64,55 @@ Execute LINQ queries against WMS entities.
|
|||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{
|
||||||
"Application": "EasyWMS",
|
"Application": "EasyWMS",
|
||||||
"QueryType": 1,
|
"QueryType": 0,
|
||||||
"Expression": "Context.{EntityType}.Select(z => z)"
|
"Expression": "Context.Products.Where(z => z.Code == \\"X\\").OrderBy(z => z.Id)",
|
||||||
|
"Take": 100
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### Supported Entity Types
|
Rules (see the query tools for details):
|
||||||
|
|
||||||
| Entity Type | Description |
|
- **\`QueryType: 0\` (Reading) is the default** — status fields are strings
|
||||||
|-------------|-------------|
|
(\`"Release"\`). \`QueryType: 1\` (Writing) exists but status fields become
|
||||||
| Products | Product references and SKUs |
|
enums there: string comparisons fail. Old examples using \`1\` must not be
|
||||||
| Containers | Pallets, boxes, and container types |
|
copied. The query tools expose this as the opt-in \`query_type\` parameter.
|
||||||
| Stocks | Available inventory by location |
|
- **\`Where\` and \`OrderBy\` go in the Expression; \`Take\`/\`Skip\` are API
|
||||||
| ProductLocations | Product placement in warehouse |
|
parameters.** \`OrderBy\` is mandatory as soon as \`Take\` is used.
|
||||||
| Tasks | WMS tasks (picks, puts, moves, etc.) |
|
- **No \`Select\` projections** — the \`Select\` parameter causes server-side
|
||||||
| Accounts | Customer accounts |
|
compile errors. Query full rows.
|
||||||
| Suppliers | Supplier information |
|
- **No relative dates** (\`DateTime.Now\`, \`AddDays()\`) — write literal dates:
|
||||||
| Kits | Product kits and bundles |
|
\`new DateTime(2026, 8, 1)\`.
|
||||||
| Aliases | Product aliases and alternative codes |
|
|
||||||
| InboundOrders | Inbound/receiving orders |
|
|
||||||
| Receptions | Actual receptions |
|
|
||||||
| OutboundOrders | Outbound/shipping orders |
|
|
||||||
|
|
||||||
### Example Queries
|
### Entity Types
|
||||||
|
|
||||||
|
Common entities: Products, Containers, Stocks, ProductLocations, Tasks,
|
||||||
|
Accounts, Suppliers, Kits, Alias (invariant — no plural form), InboundOrders,
|
||||||
|
Receptions, OutboundOrders.
|
||||||
|
|
||||||
|
**The authoritative list (288 entities) comes from \`get_entity_metadata\`**
|
||||||
|
(Metadata API) — entity names are resolved case-insensitively from the AD name
|
||||||
|
(Container) or the TableName (Containers).
|
||||||
|
|
||||||
|
### Example Expressions
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
# Get all products (limited)
|
# Filter + mandatory OrderBy (Take passed as API parameter, not in the expression)
|
||||||
Context.Products.Take(100).Select(z => z)
|
Context.Products.Where(z => z.Code.Contains("ABC")).OrderBy(z => z.Id)
|
||||||
|
|
||||||
# Get specific fields
|
# Status comparison — strings in Reading (QueryType 0)
|
||||||
Context.Products.Select(z => new { z.Id, z.Code, z.Name })
|
Context.OutboundOrders.Where(z => z.OutboundOrderStatus == "Release").OrderBy(z => z.Id)
|
||||||
|
|
||||||
# Filter and select
|
|
||||||
Context.Tasks.Where(z => z.Status == "Pending").Take(50).Select(z => z)
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### MCP Tool
|
### Counting
|
||||||
|
|
||||||
Use \`call_query_api\` tool to execute queries.
|
**Endpoint:** \`/api/QueryScalarExecute\` — same body, expression ends with
|
||||||
|
\`.Count()\` / \`.Sum(...)\`. Prefer the \`count_wms_entities\` tool for any
|
||||||
|
"how many" question.
|
||||||
|
|
||||||
|
### MCP Tools
|
||||||
|
|
||||||
|
\`query_wms_entities\`, \`count_wms_entities\`, \`call_query_api\`,
|
||||||
|
\`get_entity_schema\`, \`search_wms_data\`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -114,31 +129,7 @@ Execute commands to modify WMS data.
|
|||||||
\`\`\`json
|
\`\`\`json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Name": "CommandName, Mecalux.ITSW.EasyWMS.Modules.Contracts",
|
"Name": "Mecalux.ITSW.EasyWMS.Modules.MasterData.Contracts.Commands.ProductRemoveCommand",
|
||||||
"Properties": {
|
|
||||||
"PropertyName": "value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| ProductRemoveCommand | Remove a product |
|
|
||||||
| ProductUpdateCommand | Update product information |
|
|
||||||
| ContainerCreateCommand | Create a new container |
|
|
||||||
| TaskCancelCommand | Cancel a task |
|
|
||||||
| InboundOrderCancelCommandV2 | Cancel an inbound order |
|
|
||||||
| OutboundOrderCancelCommand | Cancel an outbound order |
|
|
||||||
|
|
||||||
### Example Command
|
|
||||||
|
|
||||||
\`\`\`json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"Name": "Mecalux.ITSW.EasyWMS.Modules.MasterData.Contracts.Commands.ProductRemoveCommand, Mecalux.ITSW.EasyWMS.Modules.Contracts",
|
|
||||||
"Properties": {
|
"Properties": {
|
||||||
"Id": "product-guid-here"
|
"Id": "product-guid-here"
|
||||||
}
|
}
|
||||||
@@ -146,6 +137,11 @@ Execute commands to modify WMS data.
|
|||||||
]
|
]
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
**\`Name\` is the \`InternalCommandName\` from the Application Dictionary, used
|
||||||
|
as-is.** Never append an assembly suffix (\`, Mecalux.ITSW...Contracts\`) — it
|
||||||
|
causes a \`FileLoadException\`. Retrieve the exact name via
|
||||||
|
\`get_ad_element_details\` before executing.
|
||||||
|
|
||||||
### MCP Tool
|
### MCP Tool
|
||||||
|
|
||||||
Use \`execute_command\` tool to execute commands.
|
Use \`execute_command\` tool to execute commands.
|
||||||
@@ -154,7 +150,7 @@ Use \`execute_command\` tool to execute commands.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Workflow API
|
## Workflow API (Application Dictionary)
|
||||||
|
|
||||||
Retrieve workflow definitions by application.
|
Retrieve workflow definitions by application.
|
||||||
|
|
||||||
@@ -165,30 +161,23 @@ Retrieve workflow definitions by application.
|
|||||||
### Request Format
|
### Request Format
|
||||||
|
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
["EasyWMS", "AD", 5000, 0]
|
["EasyWMS", "<tenant>", 5000, 0]
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Parameters:
|
Parameters (positional): application name, tenant code, page size, offset.
|
||||||
1. Application name (e.g., "EasyWMS")
|
|
||||||
2. Tenant code (e.g., "AD")
|
|
||||||
3. Page size (e.g., 5000)
|
|
||||||
4. Offset (e.g., 0 for first page)
|
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
Array of workflow objects with:
|
An envelope object \`{ "entities": [...] }\` — **not** a bare array. Each
|
||||||
- Id, Code, Name
|
workflow object carries lowercase keys: \`id\`, \`name\`, \`version\`,
|
||||||
- Category, Description
|
\`applicationName\`, \`commonInfo\` (createdBy, createDate, updateDate). There
|
||||||
- Version, Status
|
is no category, code or description field.
|
||||||
- Created, Modified
|
|
||||||
- Definition (JSON)
|
|
||||||
|
|
||||||
### MCP Tools
|
### MCP Tools
|
||||||
|
|
||||||
Use workflow tools to interact with workflows:
|
- \`search_workflows\` - Search by name
|
||||||
- \`search_workflows\` - Search by name, code, description
|
|
||||||
- \`get_workflow_details\` - Get full workflow definition
|
- \`get_workflow_details\` - Get full workflow definition
|
||||||
- \`list_workflow_categories\` - List all categories
|
- \`list_workflow_categories\` - List applications (workflows have no category field)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -197,13 +186,13 @@ Use workflow tools to interact with workflows:
|
|||||||
All APIs use OAuth 2.0 authentication.
|
All APIs use OAuth 2.0 authentication.
|
||||||
|
|
||||||
**Token Endpoint:** \`/EasySTS/OAuth/Token\`
|
**Token Endpoint:** \`/EasySTS/OAuth/Token\`
|
||||||
**Grant Types:** password, refresh_token
|
**Grant Types:** password, refresh_token (\`tenant_code\` is mandatory)
|
||||||
|
|
||||||
### Token Management
|
### Token Management
|
||||||
|
|
||||||
- Tokens expire after ~1200 seconds
|
- Tokens expire after ~1200 seconds
|
||||||
- Automatic refresh when < 1000 seconds remaining
|
- Automatic refresh when < 1000 seconds remaining
|
||||||
- Credentials configured in .env file
|
- Credentials come from the active profile (multi-profile \`.env\`)
|
||||||
|
|
||||||
The MCP server handles authentication automatically.
|
The MCP server handles authentication automatically.
|
||||||
|
|
||||||
@@ -217,16 +206,10 @@ The MCP server handles authentication automatically.
|
|||||||
- \`400\` - Bad request (invalid query/command)
|
- \`400\` - Bad request (invalid query/command)
|
||||||
- \`401\` - Unauthorized (token expired or invalid)
|
- \`401\` - Unauthorized (token expired or invalid)
|
||||||
- \`403\` - Forbidden (insufficient permissions)
|
- \`403\` - Forbidden (insufficient permissions)
|
||||||
- \`500\` - Internal server error
|
- \`500\` - Internal server error (incl. LINQ compile errors)
|
||||||
|
|
||||||
### Error Response Format
|
The response body of a 500 carries the real diagnostic (e.g. the compile
|
||||||
|
error naming the context) — MCP tools surface it in their error messages.
|
||||||
\`\`\`json
|
|
||||||
{
|
|
||||||
"error": "Error message",
|
|
||||||
"details": "Detailed error information"
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -238,24 +221,6 @@ The MCP server handles authentication automatically.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
API settings are configured via environment variables:
|
|
||||||
|
|
||||||
\`\`\`env
|
|
||||||
WMS_API_BASE_URL=https://10.255.255.2/ApplicationService/api
|
|
||||||
WMS_API_TOKEN_URL=https://10.255.255.2/EasySTS/OAuth/Token
|
|
||||||
WMS_API_TENANT=AD
|
|
||||||
WMS_API_USERNAME=your-username
|
|
||||||
WMS_API_PASSWORD=your-password
|
|
||||||
WORKFLOW_API_BASE=https://10.255.255.2/AD/api
|
|
||||||
WORKFLOW_PAGE_SIZE=5000
|
|
||||||
MAX_QUERY_ROWS=1000
|
|
||||||
QUERY_TIMEOUT=30000
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Note:** Use MCP tools to interact with these APIs. Direct API calls require proper authentication handling.
|
**Note:** Use MCP tools to interact with these APIs. Direct API calls require proper authentication handling.
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-54
@@ -7,11 +7,26 @@
|
|||||||
const apiService = require('./api-service').getInstance();
|
const apiService = require('./api-service').getInstance();
|
||||||
const profileManager = require('../config/profile-manager');
|
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 cache = {};
|
||||||
const cacheTimestamps = {};
|
const cacheTimestamps = {};
|
||||||
const CACHE_TTL = parseInt(process.env.WORKFLOW_CACHE_TTL) || 3600000; // 1 hour
|
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.
|
// Invalidate all caches when profile changes — AD elements are per-tenant.
|
||||||
profileManager.onSwitch(() => invalidateCache());
|
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) {
|
function isCacheValid(application, elementType) {
|
||||||
if (!cache[elementType] || !cacheTimestamps[elementType]) {
|
const key = cacheKey(application, elementType);
|
||||||
|
if (!cache[key] || !cacheTimestamps[key]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const age = now - cacheTimestamps[elementType];
|
const age = now - cacheTimestamps[key];
|
||||||
return age < CACHE_TTL;
|
return age < CACHE_TTL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all elements of a specific type from AD API
|
* 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} elementType - Type of element (Command, Query, Dialog, etc.)
|
||||||
|
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||||
* @returns {Promise<Array>} Array of elements
|
* @returns {Promise<Array>} Array of elements
|
||||||
*/
|
*/
|
||||||
async function getElements(elementType) {
|
async function getElements(elementType, application) {
|
||||||
// Validate element type
|
// Validate element type
|
||||||
if (!AD_ELEMENT_TYPES[elementType]) {
|
if (!AD_ELEMENT_TYPES[elementType]) {
|
||||||
throw new Error(`Unknown element type: ${elementType}. Valid types: ${Object.keys(AD_ELEMENT_TYPES).join(', ')}`);
|
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
|
// Check cache
|
||||||
if (isCacheValid(elementType)) {
|
if (isCacheValid(app, elementType)) {
|
||||||
console.error(`[AD] Cache hit: ${elementType} (${cache[elementType].length} elements)`);
|
console.error(`[AD] Cache hit: ${key} (${cache[key].length} elements)`);
|
||||||
return cache[elementType];
|
return cache[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`[AD] Cache expired or empty, fetching ${elementType}...`);
|
console.error(`[AD] Cache expired or empty, fetching ${key}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let allElements = [];
|
let allElements = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const pageSize = AD_ELEMENT_TYPES[elementType];
|
const pageSize = AD_ELEMENT_TYPES[elementType];
|
||||||
const profile = profileManager.getCurrent();
|
const tenant = profileManager.getCurrent().tenant;
|
||||||
const application = profile.application;
|
|
||||||
const tenant = profile.tenant;
|
|
||||||
|
|
||||||
while (true) {
|
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)
|
// Use AD API (useAdApi=true)
|
||||||
const response = await apiService.post(`/${elementType}/GetByApplication`, body, true);
|
const response = await apiService.post(`/${elementType}/GetByApplication`, body, true);
|
||||||
@@ -115,14 +134,14 @@ async function getElements(elementType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
cache[elementType] = allElements;
|
cache[key] = allElements;
|
||||||
cacheTimestamps[elementType] = Date.now();
|
cacheTimestamps[key] = Date.now();
|
||||||
|
|
||||||
console.error(`[AD] Successfully cached ${allElements.length} ${elementType}`);
|
console.error(`[AD] Successfully cached ${allElements.length} ${key}`);
|
||||||
return allElements;
|
return allElements;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[AD] Error fetching ${elementType}:`, error.message);
|
console.error(`[AD] Error fetching ${key}:`, error.message);
|
||||||
throw new Error(`Failed to fetch ${elementType}: ${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} elementType - Type of element
|
||||||
* @param {string} query - Search query (matches name, description, etc.)
|
* @param {string} query - Search query (matches name, description, etc.)
|
||||||
* @param {number} limit - Maximum results to return
|
* @param {number} limit - Maximum results to return
|
||||||
|
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||||
*/
|
*/
|
||||||
async function searchElements(elementType, query, limit = 50) {
|
async function searchElements(elementType, query, limit = 50, application) {
|
||||||
const elements = await getElements(elementType);
|
const elements = await getElements(elementType, application);
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return elements.slice(0, limit);
|
return elements.slice(0, limit);
|
||||||
@@ -157,9 +177,10 @@ async function searchElements(elementType, query, limit = 50) {
|
|||||||
* Get element details by ID or name
|
* Get element details by ID or name
|
||||||
* @param {string} elementType - Type of element
|
* @param {string} elementType - Type of element
|
||||||
* @param {string|number} elementId - Element ID or name
|
* @param {string|number} elementId - Element ID or name
|
||||||
|
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||||
*/
|
*/
|
||||||
async function getElementDetails(elementType, elementId) {
|
async function getElementDetails(elementType, elementId, application) {
|
||||||
const elements = await getElements(elementType);
|
const elements = await getElements(elementType, application);
|
||||||
|
|
||||||
// Try to find by Id, id, Code, code, Name, or name
|
// Try to find by Id, id, Code, code, Name, or name
|
||||||
const element = elements.find(e =>
|
const element = elements.find(e =>
|
||||||
@@ -174,45 +195,49 @@ async function getElementDetails(elementType, elementId) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!element) {
|
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;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get application summary (count of each element type)
|
* Get application summary — état des caches par (application, type) (D26).
|
||||||
* Only loads types that are already cached to avoid long wait times
|
* 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() {
|
function getApplicationSummary() {
|
||||||
const summary = {};
|
const byApplication = {};
|
||||||
|
|
||||||
Object.keys(AD_ELEMENT_TYPES).forEach(type => {
|
Object.keys(cache).forEach(key => {
|
||||||
if (cache[type]) {
|
const [app, type] = key.split('::');
|
||||||
summary[type] = {
|
if (!byApplication[app]) byApplication[app] = {};
|
||||||
count: cache[type].length,
|
byApplication[app][type] = {
|
||||||
cached: true,
|
count: cache[key].length,
|
||||||
cacheAge: cacheTimestamps[type] ? Math.floor((Date.now() - cacheTimestamps[type]) / 1000) : null
|
cacheAge: cacheTimestamps[key] ? Math.floor((Date.now() - cacheTimestamps[key]) / 1000) : null
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
summary[type] = {
|
|
||||||
count: 0,
|
|
||||||
cached: false,
|
|
||||||
cacheAge: 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) {
|
function invalidateCache(elementType = null) {
|
||||||
if (elementType) {
|
if (elementType) {
|
||||||
delete cache[elementType];
|
Object.keys(cache)
|
||||||
delete cacheTimestamps[elementType];
|
.filter(k => k.endsWith(`::${elementType}`))
|
||||||
|
.forEach(k => {
|
||||||
|
delete cache[k];
|
||||||
|
delete cacheTimestamps[k];
|
||||||
|
});
|
||||||
console.error(`[AD] Cache invalidated: ${elementType}`);
|
console.error(`[AD] Cache invalidated: ${elementType}`);
|
||||||
} else {
|
} else {
|
||||||
Object.keys(cache).forEach(k => {
|
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() {
|
function getCacheStatus() {
|
||||||
const status = {};
|
const status = {};
|
||||||
Object.keys(AD_ELEMENT_TYPES).forEach(type => {
|
Object.keys(cache).forEach(key => {
|
||||||
status[type] = {
|
const [app, type] = key.split('::');
|
||||||
cached: !!cache[type],
|
if (!status[app]) status[app] = {};
|
||||||
count: cache[type] ? cache[type].length : 0,
|
status[app][type] = {
|
||||||
timestamp: cacheTimestamps[type],
|
cached: true,
|
||||||
age: cacheTimestamps[type] ? Math.floor((Date.now() - cacheTimestamps[type]) / 1000) : null,
|
count: cache[key].length,
|
||||||
valid: isCacheValid(type)
|
timestamp: cacheTimestamps[key],
|
||||||
|
age: cacheTimestamps[key] ? Math.floor((Date.now() - cacheTimestamps[key]) / 1000) : null,
|
||||||
|
valid: isCacheValid(app, type)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return status;
|
return status;
|
||||||
|
|||||||
@@ -323,14 +323,17 @@ class APIService {
|
|||||||
* e.g. "Context.OutboundOrders.Where(z => z.OutboundOrderStatus == \"Release\").OrderBy(z => z.Id)"
|
* e.g. "Context.OutboundOrders.Where(z => z.OutboundOrderStatus == \"Release\").OrderBy(z => z.Id)"
|
||||||
*
|
*
|
||||||
* @param {string} expression - LINQ expression (Context.Entity or Context.Entity.Where(...))
|
* @param {string} expression - LINQ expression (Context.Entity or Context.Entity.Where(...))
|
||||||
* @param {object} options - { take, skip, select, orderBy, inlineCount }
|
* @param {object} options - { take, skip, select, orderBy, inlineCount, queryType }
|
||||||
*/
|
*/
|
||||||
async executeQuery(expression, options = {}) {
|
async executeQuery(expression, options = {}) {
|
||||||
const { take, skip, select, orderBy, inlineCount } = options;
|
const { take, skip, select, orderBy, inlineCount, queryType } = options;
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
Application: profileManager.getCurrent().application,
|
Application: profileManager.getCurrent().application,
|
||||||
QueryType: 0, // Reading = 0 (status fields are strings), Writing = 1 (enums)
|
// Reading = 0 par défaut (statuts en chaînes) ; Writing/Metrics en
|
||||||
|
// opt-in explicite via query_type (D25) — la garde de valeur vit dans
|
||||||
|
// wms-query-service.assertValidQueryType, pas ici.
|
||||||
|
QueryType: queryType ?? 0,
|
||||||
Expression: expression,
|
Expression: expression,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -363,11 +366,13 @@ class APIService {
|
|||||||
* Execute a scalar LINQ query (Count, Sum, etc.) via QueryScalarExecute.
|
* Execute a scalar LINQ query (Count, Sum, etc.) via QueryScalarExecute.
|
||||||
* Returns the scalar value directly.
|
* Returns the scalar value directly.
|
||||||
* @param {string} fullExpression - e.g. "Context.OutboundOrders.Where(...).Count()"
|
* @param {string} fullExpression - e.g. "Context.OutboundOrders.Where(...).Count()"
|
||||||
|
* @param {object} options - { queryType }
|
||||||
*/
|
*/
|
||||||
async executeScalarQuery(fullExpression) {
|
async executeScalarQuery(fullExpression, options = {}) {
|
||||||
const body = {
|
const body = {
|
||||||
Application: profileManager.getCurrent().application,
|
Application: profileManager.getCurrent().application,
|
||||||
QueryType: 0, // Reading = 0 — string enum names in filters (Writing=1 fails with enum comparisons)
|
// Reading = 0 par défaut — voir executeQuery / D25.
|
||||||
|
QueryType: options.queryType ?? 0,
|
||||||
Expression: fullExpression,
|
Expression: fullExpression,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -119,15 +119,22 @@ function suggestClosest(input, limit = 5) {
|
|||||||
* Résout un nom d'entité vers son TableName.
|
* Résout un nom d'entité vers son TableName.
|
||||||
*
|
*
|
||||||
* @param {string} entityType - Name AD ou TableName, insensible à la casse
|
* @param {string} entityType - Name AD ou TableName, insensible à la casse
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {boolean} [options.allowUnknown=false] - Un nom inconnu du Reading
|
||||||
|
* passe tel quel avec un warning au lieu d'échouer. Utilisé quand
|
||||||
|
* query_type != 0 (D25) : la table est construite sur le Metadata Reading,
|
||||||
|
* or le modèle Writing/Metrics peut contenir des entités hors Reading.
|
||||||
* @returns {Promise<{tableName: string, warning?: string}>}
|
* @returns {Promise<{tableName: string, warning?: string}>}
|
||||||
* - nom connu : { tableName } (le TableName exact)
|
* - nom connu : { tableName } (le TableName exact)
|
||||||
* - Metadata injoignable : { tableName: entityType, warning } — on laisse
|
* - Metadata injoignable : { tableName: entityType, warning } — on laisse
|
||||||
* passer le nom tel quel (comportement historique) plutôt que de tout
|
* passer le nom tel quel (comportement historique) plutôt que de tout
|
||||||
* bloquer, et on le dit dans la réponse
|
* bloquer, et on le dit dans la réponse
|
||||||
* @throws {Error} nom inconnu du modèle Reading — AVANT tout appel réseau de
|
* @throws {Error} nom inconnu du modèle Reading (sauf allowUnknown) — AVANT
|
||||||
* requête, avec suggestions proches et renvoi vers get_entity_metadata
|
* tout appel réseau de requête, avec suggestions proches et renvoi vers
|
||||||
|
* get_entity_metadata
|
||||||
*/
|
*/
|
||||||
async function resolveEntityType(entityType) {
|
async function resolveEntityType(entityType, options = {}) {
|
||||||
|
const { allowUnknown = false } = options;
|
||||||
if (!entityType || typeof entityType !== 'string' || entityType.trim() === '') {
|
if (!entityType || typeof entityType !== 'string' || entityType.trim() === '') {
|
||||||
throw new Error('entity_type est requis. Utilisez get_entity_metadata pour la liste des entités interrogeables.');
|
throw new Error('entity_type est requis. Utilisez get_entity_metadata pour la liste des entités interrogeables.');
|
||||||
}
|
}
|
||||||
@@ -150,6 +157,15 @@ async function resolveEntityType(entityType) {
|
|||||||
|
|
||||||
const suggestions = suggestClosest(trimmed);
|
const suggestions = suggestClosest(trimmed);
|
||||||
const closest = suggestions.length > 0 ? ` Proches : ${suggestions.join(', ')}.` : '';
|
const closest = suggestions.length > 0 ? ` Proches : ${suggestions.join(', ')}.` : '';
|
||||||
|
|
||||||
|
if (allowUnknown) {
|
||||||
|
console.error(`[EntityResolver] "${trimmed}" unknown to Reading metadata, passing through (allowUnknown)`);
|
||||||
|
return {
|
||||||
|
tableName: trimmed,
|
||||||
|
warning: `"${trimmed}" est inconnu du modèle Reading (Metadata) ; il est transmis tel quel car query_type != 0 — le contexte demandé peut contenir des entités hors Reading.${closest}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`"${trimmed}" n'existe pas dans le modèle Reading.${closest} ` +
|
`"${trimmed}" n'existe pas dans le modèle Reading.${closest} ` +
|
||||||
`${tableNames.length} entités disponibles — utilisez get_entity_metadata pour la liste.`
|
`${tableNames.length} entités disponibles — utilisez get_entity_metadata pour la liste.`
|
||||||
|
|||||||
@@ -6,6 +6,27 @@
|
|||||||
const apiService = require('./api-service').getInstance();
|
const apiService = require('./api-service').getInstance();
|
||||||
const entityResolver = require('./entity-resolver');
|
const entityResolver = require('./entity-resolver');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Garde de valeur de query_type (D25). Le wrapper D23 valide les noms de
|
||||||
|
* paramètres, pas les valeurs — cette garde s'exécute AVANT tout appel réseau
|
||||||
|
* (y compris la résolution d'entité) et nomme les quatre contextes.
|
||||||
|
* @param {*} queryType - valeur reçue de l'outil (défaut 0 si absent)
|
||||||
|
* @returns {number} la valeur validée
|
||||||
|
*/
|
||||||
|
function assertValidQueryType(queryType) {
|
||||||
|
if (queryType == null) return 0;
|
||||||
|
if (!Number.isInteger(queryType) || queryType < 0 || queryType > 3) {
|
||||||
|
throw new Error(
|
||||||
|
`query_type invalide : ${JSON.stringify(queryType)}. Valeurs acceptées : ` +
|
||||||
|
`0 = Reading (défaut — statuts en chaînes, ex. "Release"), ` +
|
||||||
|
`1 = Writing (statuts en énumérations : les comparaisons de chaînes échouent), ` +
|
||||||
|
`2 = DataWarehouse (souvent non configuré), ` +
|
||||||
|
`3 = Metrics (modèle de données distinct).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return queryType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a LINQ select expression
|
* Build a LINQ select expression
|
||||||
* @param {string} entityType - Entity type (Products, Containers, etc.)
|
* @param {string} entityType - Entity type (Products, Containers, etc.)
|
||||||
@@ -19,11 +40,19 @@ const entityResolver = require('./entity-resolver');
|
|||||||
* @param {string} selectExpression - LINQ select expression
|
* @param {string} selectExpression - LINQ select expression
|
||||||
* @param {string|null} filter - Optional filter
|
* @param {string|null} filter - Optional filter
|
||||||
* @param {number} limit - Result limit
|
* @param {number} limit - Result limit
|
||||||
|
* @param {number} queryType - QueryContextType (0 = Reading par défaut, D25)
|
||||||
*/
|
*/
|
||||||
async function queryEntities(entityType, selectExpression = 'z => z', filter = null, limit = 100) {
|
async function queryEntities(entityType, selectExpression = 'z => z', filter = null, limit = 100, queryType = 0) {
|
||||||
|
// Garde de valeur avant tout réseau (D25).
|
||||||
|
queryType = assertValidQueryType(queryType);
|
||||||
|
|
||||||
// Résolution Name/TableName -> TableName (D21). Un nom inconnu échoue ici,
|
// Résolution Name/TableName -> TableName (D21). Un nom inconnu échoue ici,
|
||||||
// avant tout appel réseau de requête — l'erreur porte les suggestions.
|
// avant tout appel réseau de requête — l'erreur porte les suggestions.
|
||||||
const { tableName, warning } = await entityResolver.resolveEntityType(entityType);
|
// En query_type != 0, un nom hors Reading passe tel quel avec warning : le
|
||||||
|
// modèle Writing/Metrics peut contenir des entités hors Reading (D25).
|
||||||
|
const { tableName, warning } = await entityResolver.resolveEntityType(entityType, {
|
||||||
|
allowUnknown: queryType !== 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enforce max limit
|
// Enforce max limit
|
||||||
@@ -39,17 +68,19 @@ async function queryEntities(entityType, selectExpression = 'z => z', filter = n
|
|||||||
// OrderBy must be embedded in the expression (not as a separate API param)
|
// OrderBy must be embedded in the expression (not as a separate API param)
|
||||||
expression += `.OrderBy(z => z.Id)`;
|
expression += `.OrderBy(z => z.Id)`;
|
||||||
|
|
||||||
console.error(`[WMSQuery] Querying ${entityType}: ${expression} | take=${actualLimit} select=${selectExpression}`);
|
console.error(`[WMSQuery] Querying ${entityType}: ${expression} | take=${actualLimit} select=${selectExpression} queryType=${queryType}`);
|
||||||
|
|
||||||
const result = await apiService.executeQuery(expression, {
|
const result = await apiService.executeQuery(expression, {
|
||||||
take: actualLimit,
|
take: actualLimit,
|
||||||
select: selectExpression !== 'z => z' ? selectExpression : undefined,
|
select: selectExpression !== 'z => z' ? selectExpression : undefined,
|
||||||
|
queryType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entityType,
|
entityType,
|
||||||
resolvedTableName: tableName,
|
resolvedTableName: tableName,
|
||||||
...(warning ? { warning } : {}),
|
...(warning ? { warning } : {}),
|
||||||
|
...(queryType !== 0 ? { queryType } : {}),
|
||||||
expression,
|
expression,
|
||||||
limit: actualLimit,
|
limit: actualLimit,
|
||||||
count: Array.isArray(result) ? result.length : 0,
|
count: Array.isArray(result) ? result.length : 0,
|
||||||
@@ -57,7 +88,9 @@ async function queryEntities(entityType, selectExpression = 'z => z', filter = n
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[WMSQuery] Query failed:`, error.message);
|
console.error(`[WMSQuery] Query failed:`, error.message);
|
||||||
throw new Error(`Query failed for ${entityType}: ${error.message}`);
|
// Le warning de résolution (nom hors Reading en query_type != 0) reste
|
||||||
|
// visible même quand le WMS échoue ensuite.
|
||||||
|
throw new Error(`Query failed for ${entityType}: ${error.message}${warning ? `\nWarning: ${warning}` : ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,11 +182,17 @@ async function getEntitySchema(entityType) {
|
|||||||
* Count entities with optional filter
|
* Count entities with optional filter
|
||||||
* @param {string} entityType - Entity type
|
* @param {string} entityType - Entity type
|
||||||
* @param {string|null} filter - Optional filter
|
* @param {string|null} filter - Optional filter
|
||||||
|
* @param {number} queryType - QueryContextType (0 = Reading par défaut, D25)
|
||||||
*/
|
*/
|
||||||
async function countEntities(entityType, filter = null) {
|
async function countEntities(entityType, filter = null, queryType = 0) {
|
||||||
|
// Garde de valeur avant tout réseau (D25).
|
||||||
|
queryType = assertValidQueryType(queryType);
|
||||||
|
|
||||||
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
||||||
// sur nom inconnu.
|
// sur nom inconnu, sauf en query_type != 0 (passage tel quel + warning, D25).
|
||||||
const { tableName, warning } = await entityResolver.resolveEntityType(entityType);
|
const { tableName, warning } = await entityResolver.resolveEntityType(entityType, {
|
||||||
|
allowUnknown: queryType !== 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build: Context.Entity.Where(...).Count()
|
// Build: Context.Entity.Where(...).Count()
|
||||||
@@ -165,20 +204,21 @@ async function countEntities(entityType, filter = null) {
|
|||||||
parts.push('Count()');
|
parts.push('Count()');
|
||||||
const fullExpression = parts.join('.');
|
const fullExpression = parts.join('.');
|
||||||
|
|
||||||
console.error(`[WMSQuery] Counting ${entityType}: ${fullExpression}`);
|
console.error(`[WMSQuery] Counting ${entityType}: ${fullExpression} | queryType=${queryType}`);
|
||||||
|
|
||||||
const count = await apiService.executeScalarQuery(fullExpression);
|
const count = await apiService.executeScalarQuery(fullExpression, { queryType });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entityType,
|
entityType,
|
||||||
resolvedTableName: tableName,
|
resolvedTableName: tableName,
|
||||||
...(warning ? { warning } : {}),
|
...(warning ? { warning } : {}),
|
||||||
|
...(queryType !== 0 ? { queryType } : {}),
|
||||||
filter,
|
filter,
|
||||||
count
|
count
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[WMSQuery] Count failed:`, error.message);
|
console.error(`[WMSQuery] Count failed:`, error.message);
|
||||||
throw new Error(`Count failed for ${entityType}: ${error.message}`);
|
throw new Error(`Count failed for ${entityType}: ${error.message}${warning ? `\nWarning: ${warning}` : ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,4 +228,5 @@ module.exports = {
|
|||||||
searchEntities,
|
searchEntities,
|
||||||
getEntitySchema,
|
getEntitySchema,
|
||||||
countEntities,
|
countEntities,
|
||||||
|
assertValidQueryType,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,19 @@
|
|||||||
* Workflow Service
|
* Workflow Service
|
||||||
* Handles workflow fetching with lazy loading and caching
|
* Handles workflow fetching with lazy loading and caching
|
||||||
* Workflows are only loaded when first requested (not at startup)
|
* 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 apiService = require('./api-service').getInstance();
|
||||||
const profileManager = require('../config/profile-manager');
|
const profileManager = require('../config/profile-manager');
|
||||||
|
|
||||||
// Cache state
|
// Cache state — un cache de workflows par application (D26)
|
||||||
let workflowCache = null;
|
let workflowCaches = {}; // application -> workflows[]
|
||||||
let cacheTimestamp = null;
|
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
|
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
|
// 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());
|
profileManager.onSwitch(() => clearCache());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if cache is still valid
|
* Application effective : celle demandée, sinon celle du profil actif.
|
||||||
*/
|
*/
|
||||||
function isCacheValid() {
|
function resolveApplication(application) {
|
||||||
if (!workflowCache || !cacheTimestamp) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const age = Date.now() - cacheTimestamps[application];
|
||||||
const age = now - cacheTimestamp;
|
|
||||||
return age < CACHE_TTL;
|
return age < CACHE_TTL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all workflows from API with pagination
|
* Fetch all workflows of an application from API with pagination.
|
||||||
* Uses high page size (5000) to minimize API calls
|
* 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
|
// Check cache validity
|
||||||
if (isCacheValid()) {
|
if (isCacheValid(app)) {
|
||||||
console.error('[Workflow] Using cached data');
|
console.error(`[Workflow] Using cached data for "${app}"`);
|
||||||
return workflowCache;
|
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 {
|
try {
|
||||||
let allWorkflows = [];
|
let allWorkflows = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const pageSize = parseInt(process.env.WORKFLOW_PAGE_SIZE) || 5000;
|
const pageSize = parseInt(process.env.WORKFLOW_PAGE_SIZE) || 5000;
|
||||||
const profile = profileManager.getCurrent();
|
const tenant = profileManager.getCurrent().tenant;
|
||||||
const application = profile.application;
|
|
||||||
const tenant = profile.tenant;
|
|
||||||
|
|
||||||
while (true) {
|
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)
|
// Use AD API (useAdApi=true)
|
||||||
const response = await apiService.post('/Workflow/GetByApplication', body, true);
|
const response = await apiService.post('/Workflow/GetByApplication', body, true);
|
||||||
@@ -80,17 +94,45 @@ async function fetchAllWorkflows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
workflowCache = allWorkflows;
|
workflowCaches[app] = allWorkflows;
|
||||||
cacheTimestamp = Date.now();
|
cacheTimestamps[app] = Date.now();
|
||||||
|
|
||||||
console.error(`[Workflow] Successfully cached ${allWorkflows.length} workflows`);
|
console.error(`[Workflow] Successfully cached ${allWorkflows.length} workflows for "${app}"`);
|
||||||
return allWorkflows;
|
return allWorkflows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Workflow] Error fetching workflows:', error.message);
|
console.error(`[Workflow] Error fetching workflows for "${app}":`, error.message);
|
||||||
throw new Error(`Failed to fetch workflows: ${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.
|
* Search workflows by query string.
|
||||||
* Real AD keys (lowercase, cf. D5): id, name, version, applicationName,
|
* 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
|
* @param {string|null} category - Optional applicationName filter (the only
|
||||||
* grouping the AD API provides)
|
* grouping the AD API provides)
|
||||||
* @param {number} limit - Maximum results to return
|
* @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) {
|
async function searchWorkflows(query, category = null, limit = 50, application) {
|
||||||
const workflows = await fetchAllWorkflows();
|
const workflows = await fetchAllWorkflows(application);
|
||||||
|
|
||||||
let results = workflows;
|
let results = workflows;
|
||||||
|
|
||||||
@@ -130,15 +173,17 @@ async function searchWorkflows(query, category = null, limit = 50) {
|
|||||||
/**
|
/**
|
||||||
* Get workflow details by ID
|
* Get workflow details by ID
|
||||||
* @param {string|number} workflowId - Workflow 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
|
// 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).
|
// workflow du cache (undefined === undefined sur les clés mortes ci-dessous).
|
||||||
if (workflowId == null || workflowId === '') {
|
if (workflowId == null || workflowId === '') {
|
||||||
throw new Error('workflow_id est requis (id ou nom exact du workflow). Utilisez search_workflows pour le trouver.');
|
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
|
// 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
|
// Id/Code/Name n'existent pas sur ces objets — les comparer faisait matcher
|
||||||
@@ -149,82 +194,65 @@ async function getWorkflowDetails(workflowId) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!workflow) {
|
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;
|
return workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List distinct applicationName values.
|
* Get workflow statistics for one application
|
||||||
* Workflows have no category field — applicationName is the only grouping the
|
* @param {string} [application] - Application AD interrogée (défaut : profil)
|
||||||
* AD API provides, and every workflow of the active application carries the
|
|
||||||
* same value (e.g. "EasyWMS").
|
|
||||||
*/
|
*/
|
||||||
async function listWorkflowCategories() {
|
async function getWorkflowStats(application) {
|
||||||
const workflows = await fetchAllWorkflows();
|
const app = resolveApplication(application);
|
||||||
|
const workflows = await fetchAllWorkflows(app);
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
application: app,
|
||||||
total: workflows.length,
|
total: workflows.length,
|
||||||
categories: categories.length,
|
cacheAge: cacheTimestamps[app] ? Math.floor((Date.now() - cacheTimestamps[app]) / 1000) : null
|
||||||
categoryCounts,
|
|
||||||
cacheAge: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear workflow cache (force refresh on next request)
|
* Clear workflow caches (force refresh on next request) — toutes applications.
|
||||||
*/
|
*/
|
||||||
function clearCache() {
|
function clearCache() {
|
||||||
workflowCache = null;
|
workflowCaches = {};
|
||||||
cacheTimestamp = null;
|
cacheTimestamps = {};
|
||||||
|
applicationsCache = null;
|
||||||
|
applicationsTimestamp = null;
|
||||||
console.error('[Workflow] Cache cleared');
|
console.error('[Workflow] Cache cleared');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cache status
|
* Get cache status, per application (D26)
|
||||||
|
* @returns {Object} application -> { cached, count, timestamp, age, valid }
|
||||||
*/
|
*/
|
||||||
function getCacheStatus() {
|
function getCacheStatus() {
|
||||||
return {
|
const status = {};
|
||||||
cached: workflowCache !== null,
|
Object.keys(workflowCaches).forEach(app => {
|
||||||
count: workflowCache ? workflowCache.length : 0,
|
status[app] = {
|
||||||
timestamp: cacheTimestamp,
|
cached: true,
|
||||||
age: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null,
|
count: workflowCaches[app].length,
|
||||||
valid: isCacheValid()
|
timestamp: cacheTimestamps[app],
|
||||||
|
age: cacheTimestamps[app] ? Math.floor((Date.now() - cacheTimestamps[app]) / 1000) : null,
|
||||||
|
valid: isCacheValid(app)
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fetchAllWorkflows,
|
fetchAllWorkflows,
|
||||||
|
fetchApplications,
|
||||||
searchWorkflows,
|
searchWorkflows,
|
||||||
getWorkflowDetails,
|
getWorkflowDetails,
|
||||||
listWorkflowCategories,
|
|
||||||
getWorkflowStats,
|
getWorkflowStats,
|
||||||
clearCache,
|
clearCache,
|
||||||
getCacheStatus
|
getCacheStatus
|
||||||
|
|||||||
+46
-31
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const adService = require('../services/ad-service');
|
const adService = require('../services/ad-service');
|
||||||
|
const workflowService = require('../services/workflow-service');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List available AD tools
|
* List available AD tools
|
||||||
@@ -12,7 +13,7 @@ function listTools() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'get_application_summary',
|
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: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -21,7 +22,7 @@ function listTools() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_ad_elements',
|
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: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -35,6 +36,10 @@ function listTools() {
|
|||||||
description: 'Maximum number of elements to return (default: 100, max: 1000)',
|
description: 'Maximum number of elements to return (default: 100, max: 1000)',
|
||||||
default: 100,
|
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'],
|
required: ['element_type'],
|
||||||
},
|
},
|
||||||
@@ -59,6 +64,10 @@ function listTools() {
|
|||||||
description: 'Maximum results (default: 50)',
|
description: 'Maximum results (default: 50)',
|
||||||
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'],
|
required: ['element_type', 'query'],
|
||||||
},
|
},
|
||||||
@@ -78,6 +87,10 @@ function listTools() {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Element ID or name',
|
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'],
|
required: ['element_type', 'element_id'],
|
||||||
},
|
},
|
||||||
@@ -140,23 +153,22 @@ async function executeTool(name, args) {
|
|||||||
async function getApplicationSummaryTool(args) {
|
async function getApplicationSummaryTool(args) {
|
||||||
console.error('[ADTools] Getting application summary');
|
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 cachedEntries = 0;
|
||||||
let totalCached = 0;
|
|
||||||
let totalElements = 0;
|
let totalElements = 0;
|
||||||
const cachedTypes = [];
|
Object.values(adByApplication).forEach(types => {
|
||||||
const uncachedTypes = [];
|
Object.values(types).forEach(info => {
|
||||||
|
cachedEntries++;
|
||||||
Object.entries(summary).forEach(([type, info]) => {
|
|
||||||
if (info.cached) {
|
|
||||||
totalCached++;
|
|
||||||
totalElements += info.count;
|
totalElements += info.count;
|
||||||
cachedTypes.push(type);
|
|
||||||
} else {
|
|
||||||
uncachedTypes.push(type);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableTypes = adService.getAvailableTypes();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
@@ -164,14 +176,14 @@ async function getApplicationSummaryTool(args) {
|
|||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
summary: {
|
summary: {
|
||||||
totalTypes: Object.keys(summary).length,
|
availableTypes: availableTypes.length,
|
||||||
cachedTypes: totalCached,
|
cachedEntries,
|
||||||
uncachedTypes: uncachedTypes.length,
|
totalElements,
|
||||||
totalElements: totalElements
|
applications: Object.keys(adByApplication)
|
||||||
},
|
},
|
||||||
elementCounts: summary,
|
adElementsByApplication: adByApplication,
|
||||||
cached: cachedTypes,
|
workflowCachesByApplication: workflowsByApplication,
|
||||||
notCached: uncachedTypes
|
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)
|
}, null, 2)
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -181,11 +193,11 @@ async function getApplicationSummaryTool(args) {
|
|||||||
* Tool: get_ad_elements
|
* Tool: get_ad_elements
|
||||||
*/
|
*/
|
||||||
async function getADElementsTool(args) {
|
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
|
// Limit results
|
||||||
const limitedElements = elements.slice(0, Math.min(limit, 1000));
|
const limitedElements = elements.slice(0, Math.min(limit, 1000));
|
||||||
@@ -208,6 +220,7 @@ async function getADElementsTool(args) {
|
|||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
elementType: element_type,
|
elementType: element_type,
|
||||||
|
...(application ? { application } : {}),
|
||||||
count: elements.length,
|
count: elements.length,
|
||||||
returned: mappedElements.length,
|
returned: mappedElements.length,
|
||||||
elements: mappedElements
|
elements: mappedElements
|
||||||
@@ -220,11 +233,11 @@ async function getADElementsTool(args) {
|
|||||||
* Tool: search_ad_elements
|
* Tool: search_ad_elements
|
||||||
*/
|
*/
|
||||||
async function searchADElementsTool(args) {
|
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
|
// Map to simplified format
|
||||||
const mappedResults = results.map(e => ({
|
const mappedResults = results.map(e => ({
|
||||||
@@ -240,6 +253,7 @@ async function searchADElementsTool(args) {
|
|||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
elementType: element_type,
|
elementType: element_type,
|
||||||
|
...(application ? { application } : {}),
|
||||||
query,
|
query,
|
||||||
count: mappedResults.length,
|
count: mappedResults.length,
|
||||||
elements: mappedResults
|
elements: mappedResults
|
||||||
@@ -252,11 +266,11 @@ async function searchADElementsTool(args) {
|
|||||||
* Tool: get_ad_element_details
|
* Tool: get_ad_element_details
|
||||||
*/
|
*/
|
||||||
async function getADElementDetailsTool(args) {
|
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 {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
@@ -264,6 +278,7 @@ async function getADElementDetailsTool(args) {
|
|||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
elementType: element_type,
|
elementType: element_type,
|
||||||
|
...(application ? { application } : {}),
|
||||||
element
|
element
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
}]
|
}]
|
||||||
|
|||||||
+23
-3
@@ -1,5 +1,6 @@
|
|||||||
const apiService = require('../services/api-service').getInstance();
|
const apiService = require('../services/api-service').getInstance();
|
||||||
const entityResolver = require('../services/entity-resolver');
|
const entityResolver = require('../services/entity-resolver');
|
||||||
|
const { assertValidQueryType } = require('../services/wms-query-service');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tools MCP pour interagir avec les APIs WMS
|
* Tools MCP pour interagir avec les APIs WMS
|
||||||
@@ -36,6 +37,11 @@ function listTools() {
|
|||||||
description: 'Limite de résultats (défaut: 100)',
|
description: 'Limite de résultats (défaut: 100)',
|
||||||
default: 100,
|
default: 100,
|
||||||
},
|
},
|
||||||
|
query_type: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'QueryContextType (défaut: 0 = Reading — statuts en chaînes, à garder sauf raison explicite). Opt-in : 1 = Writing (statuts en ÉNUMÉRATIONS — les comparaisons de chaînes comme == "Release" ÉCHOUENT), 2 = DataWarehouse (souvent non configuré), 3 = Metrics (modèle de données distinct). En query_type != 0, un nom d\'entité inconnu du Metadata Reading est transmis tel quel avec un warning.',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['entity_type'],
|
required: ['entity_type'],
|
||||||
},
|
},
|
||||||
@@ -85,12 +91,23 @@ async function executeTool(name, args) {
|
|||||||
* Tool: call_query_api
|
* Tool: call_query_api
|
||||||
*/
|
*/
|
||||||
async function callQueryAPI(args) {
|
async function callQueryAPI(args) {
|
||||||
const { entity_type, expression = 'z => z', filter, limit = 100 } = args;
|
const { entity_type, expression = 'z => z', filter, limit = 100, query_type = 0 } = args;
|
||||||
|
|
||||||
|
// Conservé hors du try : si la requête échoue ensuite côté WMS, le warning
|
||||||
|
// de résolution (nom hors Reading) reste dans la réponse d'erreur.
|
||||||
|
let resolution = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Garde de valeur avant tout réseau (D25) — le wrapper D23 ne valide pas
|
||||||
|
// les valeurs.
|
||||||
|
const queryType = assertValidQueryType(query_type);
|
||||||
|
|
||||||
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
||||||
// sur nom inconnu.
|
// sur nom inconnu, sauf en query_type != 0 (passage tel quel + warning, D25).
|
||||||
const { tableName, warning } = await entityResolver.resolveEntityType(entity_type);
|
resolution = await entityResolver.resolveEntityType(entity_type, {
|
||||||
|
allowUnknown: queryType !== 0,
|
||||||
|
});
|
||||||
|
const { tableName, warning } = resolution;
|
||||||
|
|
||||||
// Expression = Context.Entity + optional Where + OrderBy (required by EF when Take is used)
|
// Expression = Context.Entity + optional Where + OrderBy (required by EF when Take is used)
|
||||||
let linqExpression = `Context.${tableName}`;
|
let linqExpression = `Context.${tableName}`;
|
||||||
@@ -103,6 +120,7 @@ async function callQueryAPI(args) {
|
|||||||
const result = await apiService.executeQuery(linqExpression, {
|
const result = await apiService.executeQuery(linqExpression, {
|
||||||
take: limit || undefined,
|
take: limit || undefined,
|
||||||
select: expression !== 'z => z' ? expression : undefined,
|
select: expression !== 'z => z' ? expression : undefined,
|
||||||
|
queryType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -115,6 +133,7 @@ async function callQueryAPI(args) {
|
|||||||
entityType: entity_type,
|
entityType: entity_type,
|
||||||
resolvedTableName: tableName,
|
resolvedTableName: tableName,
|
||||||
...(warning ? { warning } : {}),
|
...(warning ? { warning } : {}),
|
||||||
|
...(queryType !== 0 ? { queryType } : {}),
|
||||||
result,
|
result,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@@ -132,6 +151,7 @@ async function callQueryAPI(args) {
|
|||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: err.message,
|
error: err.message,
|
||||||
|
...(resolution?.warning ? { warning: resolution.warning } : {}),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function listTools() {
|
|||||||
{
|
{
|
||||||
name: 'query_wms_entities',
|
name: 'query_wms_entities',
|
||||||
description: `Query WMS entities using LINQ expressions. Returns rows (up to 1000).
|
description: `Query WMS entities using LINQ expressions. Returns rows (up to 1000).
|
||||||
Uses QueryExecute with QueryType=Reading — status fields are STRINGS (enum names, not integers).
|
Uses QueryExecute with QueryType=Reading by default — status fields are STRINGS (enum names, not integers). Other contexts via query_type (opt-in, see the parameter warning).
|
||||||
Common entities: Products, Containers, Tasks, Stocks, ProductLocations, Locations, InboundOrders, OutboundOrders, Receptions, Accounts, Suppliers, Kits, Alias. Full list via get_entity_metadata.
|
Common entities: Products, Containers, Tasks, Stocks, ProductLocations, Locations, InboundOrders, OutboundOrders, Receptions, Accounts, Suppliers, Kits, Alias. Full list via get_entity_metadata.
|
||||||
entity_type accepts the AD entity name (Container) or the TableName (Containers), case-insensitive — resolved via the Metadata API.
|
entity_type accepts the AD entity name (Container) or the TableName (Containers), case-insensitive — resolved via the Metadata API.
|
||||||
|
|
||||||
@@ -44,6 +44,11 @@ Never guess enum string values — they differ between Reading and Writing model
|
|||||||
description: 'Maximum results to return (default: 100, max: 1000)',
|
description: 'Maximum results to return (default: 100, max: 1000)',
|
||||||
default: 100,
|
default: 100,
|
||||||
},
|
},
|
||||||
|
query_type: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'QueryContextType (default: 0 = Reading — status fields are strings, keep it unless you know why). Opt-in: 1 = Writing (status fields become ENUMS — string comparisons like == "Release" FAIL), 2 = DataWarehouse (often not configured), 3 = Metrics (different data model). Entity names unknown to the Reading metadata are passed through as-is with a warning when query_type != 0.',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['entity_type'],
|
required: ['entity_type'],
|
||||||
},
|
},
|
||||||
@@ -96,6 +101,11 @@ Verified values (curl-tested):
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Optional LINQ filter condition. Status fields are strings (enum names from Reading model). Always verify enum values via docs://entities/ before use.',
|
description: 'Optional LINQ filter condition. Status fields are strings (enum names from Reading model). Always verify enum values via docs://entities/ before use.',
|
||||||
},
|
},
|
||||||
|
query_type: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'QueryContextType (default: 0 = Reading — status fields are strings, keep it unless you know why). Opt-in: 1 = Writing (status fields become ENUMS — string comparisons like == "Release" FAIL), 2 = DataWarehouse (often not configured), 3 = Metrics (different data model). Entity names unknown to the Reading metadata are passed through as-is with a warning when query_type != 0.',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['entity_type'],
|
required: ['entity_type'],
|
||||||
},
|
},
|
||||||
@@ -169,15 +179,16 @@ async function executeTool(name, args) {
|
|||||||
* Tool: query_wms_entities
|
* Tool: query_wms_entities
|
||||||
*/
|
*/
|
||||||
async function queryWmsEntities(args) {
|
async function queryWmsEntities(args) {
|
||||||
const { entity_type, select_expression = 'z => z', filter, limit = 100 } = args;
|
const { entity_type, select_expression = 'z => z', filter, limit = 100, query_type = 0 } = args;
|
||||||
|
|
||||||
console.error(`[WMSQueryTools] Querying ${entity_type}: limit=${limit}`);
|
console.error(`[WMSQueryTools] Querying ${entity_type}: limit=${limit} query_type=${query_type}`);
|
||||||
|
|
||||||
const result = await wmsQueryService.queryEntities(
|
const result = await wmsQueryService.queryEntities(
|
||||||
entity_type,
|
entity_type,
|
||||||
select_expression,
|
select_expression,
|
||||||
filter,
|
filter,
|
||||||
limit
|
limit,
|
||||||
|
query_type
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -195,11 +206,11 @@ async function queryWmsEntities(args) {
|
|||||||
* Tool: count_wms_entities
|
* Tool: count_wms_entities
|
||||||
*/
|
*/
|
||||||
async function countWmsEntities(args) {
|
async function countWmsEntities(args) {
|
||||||
const { entity_type, filter } = args;
|
const { entity_type, filter, query_type = 0 } = args;
|
||||||
|
|
||||||
console.error(`[WMSQueryTools] Counting ${entity_type}${filter ? ` where ${filter}` : ''}`);
|
console.error(`[WMSQueryTools] Counting ${entity_type}${filter ? ` where ${filter}` : ''} query_type=${query_type}`);
|
||||||
|
|
||||||
const result = await wmsQueryService.countEntities(entity_type, filter || null);
|
const result = await wmsQueryService.countEntities(entity_type, filter || null, query_type);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
|
|||||||
+49
-20
@@ -12,7 +12,7 @@ function listTools() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'search_workflows',
|
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: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -23,26 +23,34 @@ function listTools() {
|
|||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
type: 'string',
|
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: {
|
limit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Maximum results to return (default: 50)',
|
description: 'Maximum results to return (default: 50)',
|
||||||
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',
|
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: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
workflow_id: {
|
workflow_id: {
|
||||||
type: 'string',
|
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'],
|
required: ['workflow_id'],
|
||||||
@@ -50,11 +58,16 @@ function listTools() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'list_workflow_categories',
|
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: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
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
|
* Tool: search_workflows
|
||||||
*/
|
*/
|
||||||
async function searchWorkflows(args) {
|
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 {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
|
...(application ? { application } : {}),
|
||||||
count: results.length,
|
count: results.length,
|
||||||
// Clés réelles de l'API AD (minuscules, cf. D5) : id, name, version,
|
// Clés réelles de l'API AD (minuscules, cf. D5) : id, name, version,
|
||||||
// applicationName, commonInfo. Pas de code/category/description.
|
// applicationName, commonInfo. Pas de code/category/description.
|
||||||
@@ -133,11 +147,11 @@ async function searchWorkflows(args) {
|
|||||||
* Tool: get_workflow_details
|
* Tool: get_workflow_details
|
||||||
*/
|
*/
|
||||||
async function getWorkflowDetails(args) {
|
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 {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
@@ -154,22 +168,37 @@ async function getWorkflowDetails(args) {
|
|||||||
* Tool: list_workflow_categories
|
* Tool: list_workflow_categories
|
||||||
*/
|
*/
|
||||||
async function listWorkflowCategories(args) {
|
async function listWorkflowCategories(args) {
|
||||||
console.error('[WorkflowTools] Listing workflow categories');
|
const { application } = args || {};
|
||||||
|
|
||||||
const categories = await workflowService.listWorkflowCategories();
|
console.error(`[WorkflowTools] Listing applications (workflow groupings), application=${application || '(profil)'}`);
|
||||||
const stats = await workflowService.getWorkflowStats();
|
|
||||||
|
// 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 {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
success: true,
|
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.',
|
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.',
|
||||||
totalCategories: categories.length,
|
totalApplications: applications.length,
|
||||||
categories,
|
applications: enriched,
|
||||||
stats: {
|
loaded: {
|
||||||
|
application: stats.application,
|
||||||
totalWorkflows: stats.total,
|
totalWorkflows: stats.total,
|
||||||
categoryCounts: stats.categoryCounts,
|
|
||||||
cacheAge: stats.cacheAge
|
cacheAge: stats.cacheAge
|
||||||
}
|
}
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
|
|||||||
Reference in New Issue
Block a user