@deerdesign/deertools 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -16
- package/docs/prezentacja-it-deertools.html +645 -0
- package/docs/prezentacja-it-deertools.md +182 -0
- package/package.json +2 -1
- package/src/audit.js +65 -0
- package/src/baselinker/browser-login.js +214 -0
- package/src/baselinker/catalog-tools.js +457 -0
- package/src/baselinker/client.js +136 -0
- package/src/baselinker/crawler.js +157 -0
- package/src/baselinker/index.js +24 -0
- package/src/baselinker/mcp-server.js +153 -0
- package/src/baselinker/methods-index.json +1226 -0
- package/src/baselinker/schema-cache.json +6342 -0
- package/src/baselinker/ui.js +386 -0
- package/src/baselinker-commands.js +180 -0
- package/src/cli.js +22 -2
- package/src/commands.js +302 -35
- package/src/completion.js +189 -0
- package/src/credentials.js +39 -2
- package/src/doctor.js +286 -0
- package/src/domain-commands.js +288 -0
- package/src/formatters.js +50 -0
- package/src/mcp-stdio.js +197 -0
- package/src/profiles.js +78 -0
- package/src/registry.js +22 -1
- package/src/skill-commands.js +223 -0
- package/src/skills-hub.js +201 -0
- package/src/tui.js +139 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { getClient } = require('./client');
|
|
3
|
+
const { searchMethods, getMethodSchema } = require('./crawler');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Definicje narzędzi MCP dla zaawansowanego zarządzania katalogiem BaseLinker.
|
|
7
|
+
*/
|
|
8
|
+
const CATALOG_MCP_TOOLS = [
|
|
9
|
+
{
|
|
10
|
+
name: 'bl_catalog_get_product',
|
|
11
|
+
description: 'Pobiera pełne, szczegółowe dane produktu z katalogu BaseLinker: opisy, parametry, cechy, galerię zdjęć, relacje z innymi produktami, ceny, stany magazynowe i warianty.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker (z getInventories).' },
|
|
16
|
+
product_id: { type: 'integer', description: 'ID produktu w BaseLinkerze.' },
|
|
17
|
+
include_relations: { type: 'boolean', default: true, description: 'Czy dołączyć relacje z innymi produktami (related, substitution, upsell, downsell, crosssell).' },
|
|
18
|
+
include_channels_media: { type: 'boolean', default: true, description: 'Czy dołączyć zdjęcia i wideo per kanał sprzedaży.' }
|
|
19
|
+
},
|
|
20
|
+
required: ['inventory_id', 'product_id']
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'bl_catalog_search_products',
|
|
25
|
+
description: 'Wyszukuje i listuje produkty z katalogu BaseLinker wg nazwy, SKU, EAN, kategorii lub producenta.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
30
|
+
filter_category_id: { type: 'integer', description: 'Opcjonalne filtrowanie po ID kategorii.' },
|
|
31
|
+
filter_manufacturer_id: { type: 'integer', description: 'Opcjonalne filtrowanie po ID producenta.' },
|
|
32
|
+
filter_sku: { type: 'string', description: 'Opcjonalne wyszukiwanie po SKU produktu.' },
|
|
33
|
+
filter_ean: { type: 'string', description: 'Opcjonalne wyszukiwanie po kodzie EAN.' },
|
|
34
|
+
filter_name: { type: 'string', description: 'Opcjonalne wyszukiwanie po fragmencie nazwy.' },
|
|
35
|
+
page: { type: 'integer', default: 1, description: 'Numer strony wyników (po 1000 produktów).' }
|
|
36
|
+
},
|
|
37
|
+
required: ['inventory_id']
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'bl_catalog_create_or_update_product',
|
|
42
|
+
description: 'Tworzy nowy produkt lub aktualizuje istniejący w katalogu BaseLinker (SKU, EAN, waga, wymiary, stawka VAT, kategoria, producent, ceny, stany magazynowe).',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
47
|
+
product_id: { type: 'integer', description: 'ID produktu (podaj tylko przy aktualizacji; puste przy tworzeniu nowego).' },
|
|
48
|
+
parent_id: { type: 'integer', description: 'ID produktu nadrzędnego (gdy ten produkt jest wariantem).' },
|
|
49
|
+
sku: { type: 'string', description: 'Kod SKU produktu.' },
|
|
50
|
+
ean: { type: 'string', description: 'Kod kreskowy EAN.' },
|
|
51
|
+
name: { type: 'string', description: 'Domyślna nazwa produktu (do 200 znaków).' },
|
|
52
|
+
category_id: { type: 'integer', description: 'ID kategorii w katalogu.' },
|
|
53
|
+
manufacturer_id: { type: 'integer', description: 'ID producenta.' },
|
|
54
|
+
tax_rate: { type: 'number', description: 'Stawka VAT np. 23 (lub -1 dla zwolniony).' },
|
|
55
|
+
weight: { type: 'number', description: 'Waga w kg.' },
|
|
56
|
+
height: { type: 'number', description: 'Wysokość w cm.' },
|
|
57
|
+
width: { type: 'number', description: 'Szerokość w cm.' },
|
|
58
|
+
length: { type: 'number', description: 'Długość w cm.' },
|
|
59
|
+
prices: { type: 'object', description: 'Ceny brutto wg grup cenowych, np. {"105": 149.99}.' },
|
|
60
|
+
stock: { type: 'object', description: 'Stany magazynowe wg magazynów, np. {"bl_123": 25}.' },
|
|
61
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Lista tagów przypisanych do produktu.' }
|
|
62
|
+
},
|
|
63
|
+
required: ['inventory_id']
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'bl_catalog_set_descriptions_and_features',
|
|
68
|
+
description: 'Ustawia opisy (HTML lub tekst) oraz cechy/parametry produktu w BaseLinkerze (np. Kolor, Materiał, Wykończenie, Styl).',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
73
|
+
product_id: { type: 'integer', description: 'ID produktu.' },
|
|
74
|
+
name: { type: 'string', description: 'Nazwa produktu.' },
|
|
75
|
+
description: { type: 'string', description: 'Pełny opis produktu (HTML lub Markdown).' },
|
|
76
|
+
description_extra1: { type: 'string', description: 'Dodatkowy opis 1.' },
|
|
77
|
+
description_extra2: { type: 'string', description: 'Dodatkowy opis 2.' },
|
|
78
|
+
features: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
description: 'Słownik cech i parametrów produktu (np. {"Kolor": "Dąb Bielony", "Kolekcja": "Modern Life", "Klasa ścieralności": "AC5"}).'
|
|
81
|
+
},
|
|
82
|
+
lang: { type: 'string', default: '', description: 'Kod języka (np. "pl", "en", "de"). Pozostaw puste dla języka domyślnego.' }
|
|
83
|
+
},
|
|
84
|
+
required: ['inventory_id', 'product_id']
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'bl_catalog_manage_images',
|
|
89
|
+
description: 'Zarządza galerią zdjęć produktu w BaseLinkerze: dodaje nowe zdjęcia (URL lub base64) na pozycjach 0-15, podmienia zdjęcia lub usuwa wybrane pozycje.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
94
|
+
product_id: { type: 'integer', description: 'ID produktu.' },
|
|
95
|
+
images_to_set: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
description: 'Mapa pozycji (0 do 15) do URL zdjęcia lub base64, np. {"0": "https://domena.pl/foto1.jpg", "1": "https://domena.pl/foto2.jpg"}. Wartość "url:https://..." lub czysty URL "https://...".'
|
|
98
|
+
},
|
|
99
|
+
positions_to_delete: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: { type: 'integer' },
|
|
102
|
+
description: 'Lista numerów pozycji zdjęć (0-15) do bezwzględnego usunięcia z galerii.'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
required: ['inventory_id', 'product_id']
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'bl_catalog_set_relations',
|
|
110
|
+
description: 'Buduje relacje i powiązania między produktami w BaseLinkerze: produkty powiązane (related), zamienniki (substitution), upsell, downsell i crosssell.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
115
|
+
product_id: { type: 'integer', description: 'ID produktu bazowego, dla którego definiujemy powiązania.' },
|
|
116
|
+
relations: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
description: 'Kompletna lista relacji produktu z innymi produktami w katalogu.',
|
|
119
|
+
items: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
product_id: { type: 'integer', description: 'ID powiązanego produktu.' },
|
|
123
|
+
relation_type: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
enum: ['related', 'substitution', 'upsell', 'downsell', 'crosssell'],
|
|
126
|
+
description: 'Typ relacji: related (powiązany), substitution (zamiennik), upsell, downsell, crosssell (akcesorium).'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
required: ['product_id', 'relation_type']
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
required: ['inventory_id', 'product_id', 'relations']
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'bl_catalog_manage_bundle',
|
|
138
|
+
description: 'Tworzy lub aktualizuje zestaw produktów (komplet/bundle) w BaseLinkerze ze wskazaniem składników i ich ilości.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
143
|
+
product_id: { type: 'integer', description: 'ID produktu-zestawu.' },
|
|
144
|
+
bundle_products: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
description: 'Mapa ID produktów wchodzących w skład zestawu i ich ilości, np. {"12345": 1, "67890": 2}.'
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
required: ['inventory_id', 'product_id', 'bundle_products']
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'bl_catalog_categories',
|
|
154
|
+
description: 'Pobiera listę kategorii (lub tworzy/aktualizuje/usuwa kategorię) w katalogu BaseLinker.',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
159
|
+
action: { type: 'string', enum: ['list', 'add_or_update', 'delete'], default: 'list' },
|
|
160
|
+
category_id: { type: 'integer', description: 'ID kategorii (do edycji lub usunięcia).' },
|
|
161
|
+
name: { type: 'string', description: 'Nazwa kategorii (dla add_or_update).' },
|
|
162
|
+
parent_id: { type: 'integer', default: 0, description: 'ID kategorii nadrzędnej (0 dla kategorii głównej).' }
|
|
163
|
+
},
|
|
164
|
+
required: ['inventory_id']
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'bl_catalog_manufacturers',
|
|
169
|
+
description: 'Pobiera listę producentów (lub dodaje/usuwa producenta) w katalogu BaseLinker.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
action: { type: 'string', enum: ['list', 'add_or_update', 'delete'], default: 'list' },
|
|
174
|
+
manufacturer_id: { type: 'integer', description: 'ID producenta (do aktualizacji lub usunięcia).' },
|
|
175
|
+
name: { type: 'string', description: 'Nazwa producenta (dla add_or_update).' }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'bl_catalog_tags',
|
|
181
|
+
description: 'Pobiera listę tagów (lub tworzy/usuwa tag) w katalogu BaseLinker.',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
186
|
+
action: { type: 'string', enum: ['list', 'add', 'delete'], default: 'list' },
|
|
187
|
+
tag_id: { type: 'integer', description: 'ID tagu (do usunięcia).' },
|
|
188
|
+
name: { type: 'string', description: 'Nazwa nowego tagu.' }
|
|
189
|
+
},
|
|
190
|
+
required: ['inventory_id']
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'bl_catalog_update_prices_batch',
|
|
195
|
+
description: 'Masowa aktualizacja cen brutto produktów (i/lub wariantów) w katalogu BaseLinker (do 1000 produktów na raz).',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
200
|
+
products: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
description: 'Mapa { [product_id]: { [price_group_id]: cena_brutto } }, np. {"1234": {"105": 189.99}, "1235": {"105": 249.00}}.'
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
required: ['inventory_id', 'products']
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'bl_catalog_update_stock_batch',
|
|
210
|
+
description: 'Masowa aktualizacja stanów magazynowych produktów (i/lub wariantów) w katalogu BaseLinker (do 1000 produktów na raz).',
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
|
|
215
|
+
products: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
description: 'Mapa { [product_id]: { [warehouse_id]: ilosc_sztuk } }, np. {"1234": {"bl_123": 50}, "1235": {"bl_123": 12}}.'
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
required: ['inventory_id', 'products']
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'bl_catalog_get_parameters',
|
|
225
|
+
description: 'Pobiera słownik predefiniowanych parametrów i cech z katalogu BaseLinker (wraz z tłumaczeniami i opcjami wyboru).',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' }
|
|
230
|
+
},
|
|
231
|
+
required: ['inventory_id']
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'bl_search_methods',
|
|
236
|
+
description: 'Wyszukuje i podpowiada dowolną z 204 metod oficjalnego API BaseLinkera wg nazwy, kategorii lub opisu, zwracając dokładny schemat parametrów.',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
query: { type: 'string', description: 'Fraza wyszukiwania np. "order", "invoice", "stock", "label", "package".' },
|
|
241
|
+
category: { type: 'string', description: 'Opcjonalna kategoria: "Orders", "Product catalog", "Courier shipments", "Invoices", "Clients (CRM)".' }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'bl_call_api',
|
|
247
|
+
description: 'Uniwersalny interfejs do wywołania DOWOLNEJ z 204 oficjalnych metod API BaseLinkera (np. getOrders, createPackage, addInvoice itp.) z dowolnymi parametrami.',
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
method: { type: 'string', description: 'Dokładna nazwa metody z https://api.baselinker.com/ (np. getOrders, setOrderFields, getLabel).' },
|
|
252
|
+
parameters: { type: 'object', description: 'Obiekt z parametrami metody w formacie JSON.' }
|
|
253
|
+
},
|
|
254
|
+
required: ['method']
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Wykonuje wywołanie dedykowanego narzędzia katalogowego.
|
|
261
|
+
*/
|
|
262
|
+
async function handleCatalogToolCall(toolName, args, client = null) {
|
|
263
|
+
const bl = client || getClient();
|
|
264
|
+
|
|
265
|
+
switch (toolName) {
|
|
266
|
+
case 'bl_catalog_get_product': {
|
|
267
|
+
const { inventory_id, product_id, include_relations = true, include_channels_media = true } = args;
|
|
268
|
+
const res = await bl.call('getInventoryProductsData', {
|
|
269
|
+
inventory_id,
|
|
270
|
+
products: [product_id],
|
|
271
|
+
include_relations,
|
|
272
|
+
include_channels_media,
|
|
273
|
+
include_additional_eans: true,
|
|
274
|
+
include_suppliers: true
|
|
275
|
+
});
|
|
276
|
+
const product = res.products && (res.products[product_id] || Object.values(res.products)[0]);
|
|
277
|
+
if (!product) throw new Error(`Nie znaleziono produktu o ID ${product_id} w katalogu ${inventory_id}.`);
|
|
278
|
+
return product;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case 'bl_catalog_search_products': {
|
|
282
|
+
const { inventory_id, filter_category_id, filter_manufacturer_id, filter_sku, filter_ean, filter_name, page = 1 } = args;
|
|
283
|
+
const params = { inventory_id, page };
|
|
284
|
+
if (filter_category_id) params.filter_category_id = filter_category_id;
|
|
285
|
+
if (filter_manufacturer_id) params.filter_manufacturer_id = filter_manufacturer_id;
|
|
286
|
+
if (filter_sku) params.filter_sku = filter_sku;
|
|
287
|
+
if (filter_ean) params.filter_ean = filter_ean;
|
|
288
|
+
if (filter_name) params.filter_name = filter_name;
|
|
289
|
+
return await bl.call('getInventoryProductsList', params);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case 'bl_catalog_create_or_update_product': {
|
|
293
|
+
const params = { ...args };
|
|
294
|
+
return await bl.call('addInventoryProduct', params);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case 'bl_catalog_set_descriptions_and_features': {
|
|
298
|
+
const { inventory_id, product_id, name, description, description_extra1, description_extra2, features, lang = '' } = args;
|
|
299
|
+
const text_fields = {};
|
|
300
|
+
const keySuffix = lang ? `|${lang}` : '';
|
|
301
|
+
|
|
302
|
+
if (name !== undefined) text_fields[`name${keySuffix}`] = name;
|
|
303
|
+
if (description !== undefined) text_fields[`description${keySuffix}`] = description;
|
|
304
|
+
if (description_extra1 !== undefined) text_fields[`description_extra1${keySuffix}`] = description_extra1;
|
|
305
|
+
if (description_extra2 !== undefined) text_fields[`description_extra2${keySuffix}`] = description_extra2;
|
|
306
|
+
if (features !== undefined) text_fields[`features${keySuffix}`] = features;
|
|
307
|
+
|
|
308
|
+
return await bl.call('addInventoryProduct', {
|
|
309
|
+
inventory_id,
|
|
310
|
+
product_id,
|
|
311
|
+
text_fields
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case 'bl_catalog_manage_images': {
|
|
316
|
+
const { inventory_id, product_id, images_to_set = {}, positions_to_delete = [] } = args;
|
|
317
|
+
const imagesPayload = {};
|
|
318
|
+
|
|
319
|
+
for (const [pos, val] of Object.entries(images_to_set)) {
|
|
320
|
+
if (!val) {
|
|
321
|
+
imagesPayload[pos] = '';
|
|
322
|
+
} else if (val.startsWith('url:') || val.startsWith('data:')) {
|
|
323
|
+
imagesPayload[pos] = val;
|
|
324
|
+
} else {
|
|
325
|
+
imagesPayload[pos] = `url:${val}`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const pos of positions_to_delete) {
|
|
330
|
+
imagesPayload[pos] = '';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return await bl.call('addInventoryProduct', {
|
|
334
|
+
inventory_id,
|
|
335
|
+
product_id,
|
|
336
|
+
images: imagesPayload
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case 'bl_catalog_set_relations': {
|
|
341
|
+
const { inventory_id, product_id, relations } = args;
|
|
342
|
+
return await bl.call('addInventoryProduct', {
|
|
343
|
+
inventory_id,
|
|
344
|
+
product_id,
|
|
345
|
+
relations
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case 'bl_catalog_manage_bundle': {
|
|
350
|
+
const { inventory_id, product_id, bundle_products } = args;
|
|
351
|
+
return await bl.call('addInventoryProduct', {
|
|
352
|
+
inventory_id,
|
|
353
|
+
product_id,
|
|
354
|
+
is_bundle: true,
|
|
355
|
+
bundle_products
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case 'bl_catalog_categories': {
|
|
360
|
+
const { inventory_id, action = 'list', category_id, name, parent_id = 0 } = args;
|
|
361
|
+
if (action === 'list') {
|
|
362
|
+
return await bl.call('getInventoryCategories', { inventory_id });
|
|
363
|
+
}
|
|
364
|
+
if (action === 'add_or_update') {
|
|
365
|
+
const payload = { inventory_id, name, parent_id };
|
|
366
|
+
if (category_id) payload.category_id = category_id;
|
|
367
|
+
return await bl.call('addInventoryCategory', payload);
|
|
368
|
+
}
|
|
369
|
+
if (action === 'delete') {
|
|
370
|
+
if (!category_id) throw new Error('Wymagany parametr category_id do usunięcia kategorii.');
|
|
371
|
+
return await bl.call('deleteInventoryCategory', { category_id });
|
|
372
|
+
}
|
|
373
|
+
throw new Error(`Nieznana akcja: ${action}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
case 'bl_catalog_manufacturers': {
|
|
377
|
+
const { action = 'list', manufacturer_id, name } = args;
|
|
378
|
+
if (action === 'list') {
|
|
379
|
+
return await bl.call('getInventoryManufacturers', {});
|
|
380
|
+
}
|
|
381
|
+
if (action === 'add_or_update') {
|
|
382
|
+
const payload = { name };
|
|
383
|
+
if (manufacturer_id) payload.manufacturer_id = manufacturer_id;
|
|
384
|
+
return await bl.call('addInventoryManufacturer', payload);
|
|
385
|
+
}
|
|
386
|
+
if (action === 'delete') {
|
|
387
|
+
if (!manufacturer_id) throw new Error('Wymagany parametr manufacturer_id do usunięcia producenta.');
|
|
388
|
+
return await bl.call('deleteInventoryManufacturer', { manufacturer_id });
|
|
389
|
+
}
|
|
390
|
+
throw new Error(`Nieznana akcja: ${action}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
case 'bl_catalog_tags': {
|
|
394
|
+
const { inventory_id, action = 'list', tag_id, name } = args;
|
|
395
|
+
if (action === 'list') {
|
|
396
|
+
return await bl.call('getInventoryTags', { inventory_id });
|
|
397
|
+
}
|
|
398
|
+
if (action === 'add') {
|
|
399
|
+
if (!name) throw new Error('Wymagana nazwa tagu.');
|
|
400
|
+
return await bl.call('addInventoryTag', { inventory_id, name });
|
|
401
|
+
}
|
|
402
|
+
if (action === 'delete') {
|
|
403
|
+
if (!tag_id) throw new Error('Wymagany tag_id.');
|
|
404
|
+
return await bl.call('deleteInventoryTag', { tag_id });
|
|
405
|
+
}
|
|
406
|
+
throw new Error(`Nieznana akcja: ${action}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'bl_catalog_update_prices_batch': {
|
|
410
|
+
const { inventory_id, products } = args;
|
|
411
|
+
return await bl.call('updateInventoryProductsPrices', {
|
|
412
|
+
inventory_id,
|
|
413
|
+
products
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case 'bl_catalog_update_stock_batch': {
|
|
418
|
+
const { inventory_id, products } = args;
|
|
419
|
+
return await bl.call('updateInventoryProductsStock', {
|
|
420
|
+
inventory_id,
|
|
421
|
+
products
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
case 'bl_catalog_get_parameters': {
|
|
426
|
+
const { inventory_id } = args;
|
|
427
|
+
return await bl.call('getInventoryParameters', { inventory_id });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
case 'bl_search_methods': {
|
|
431
|
+
const { query, category } = args;
|
|
432
|
+
const found = searchMethods(query, category);
|
|
433
|
+
return {
|
|
434
|
+
count: found.length,
|
|
435
|
+
methods: found.slice(0, 20).map(m => ({
|
|
436
|
+
name: m.name,
|
|
437
|
+
category: m.category,
|
|
438
|
+
description: m.description,
|
|
439
|
+
parameters: m.parameters || {}
|
|
440
|
+
}))
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
case 'bl_call_api': {
|
|
445
|
+
const { method, parameters = {} } = args;
|
|
446
|
+
return await bl.call(method, parameters);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
default:
|
|
450
|
+
throw new Error(`Nieobsługiwane narzędzie BaseLinker: ${toolName}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
CATALOG_MCP_TOOLS,
|
|
456
|
+
handleCatalogToolCall
|
|
457
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const http = require('https');
|
|
3
|
+
const { getBaseLinkerToken } = require('../credentials');
|
|
4
|
+
const { logAudit } = require('../audit');
|
|
5
|
+
|
|
6
|
+
const BASELINKER_API_URL = 'https://api.baselinker.com/connector.php';
|
|
7
|
+
const RATE_LIMIT_MAX = 95; // max 95 na 60s (oficjalny limit to 100 req/min, margines 5 dla bezpieczeństwa)
|
|
8
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
9
|
+
|
|
10
|
+
class BaseLinkerClient {
|
|
11
|
+
constructor(token = null) {
|
|
12
|
+
this.token = token;
|
|
13
|
+
this.callTimestamps = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getToken() {
|
|
17
|
+
return this.token || (typeof getBaseLinkerToken === 'function' ? getBaseLinkerToken() : null) || process.env.BASELINKER_TOKEN || process.env.BL_TOKEN;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getRateLimitStatus() {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
this.callTimestamps = this.callTimestamps.filter(t => now - t < RATE_LIMIT_WINDOW_MS);
|
|
23
|
+
const used = this.callTimestamps.length;
|
|
24
|
+
const remaining = Math.max(0, RATE_LIMIT_MAX - used);
|
|
25
|
+
const oldest = this.callTimestamps[0] || now;
|
|
26
|
+
const resetInMs = Math.max(0, RATE_LIMIT_WINDOW_MS - (now - oldest));
|
|
27
|
+
return { used, max: 100, remaining, resetInMs: Math.round(resetInMs / 1000) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async waitForRateLimitSlot() {
|
|
31
|
+
while (true) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
this.callTimestamps = this.callTimestamps.filter(t => now - t < RATE_LIMIT_WINDOW_MS);
|
|
34
|
+
if (this.callTimestamps.length < RATE_LIMIT_MAX) {
|
|
35
|
+
this.callTimestamps.push(now);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const waitMs = Math.max(100, RATE_LIMIT_WINDOW_MS - (now - this.callTimestamps[0]) + 50);
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async call(method, parameters = {}) {
|
|
44
|
+
const token = this.getToken();
|
|
45
|
+
if (!token) {
|
|
46
|
+
throw new Error('Brak tokenu BaseLinker. Ustaw go za pomocą "deertools baselinker auth <token>" lub zmienną środowiskową BASELINKER_TOKEN.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await this.waitForRateLimitSlot();
|
|
50
|
+
|
|
51
|
+
const start = Date.now();
|
|
52
|
+
const postData = new URLSearchParams({
|
|
53
|
+
method,
|
|
54
|
+
parameters: JSON.stringify(parameters)
|
|
55
|
+
}).toString();
|
|
56
|
+
|
|
57
|
+
let resBody = '';
|
|
58
|
+
let httpStatus = 0;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(BASELINKER_API_URL, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
65
|
+
'X-BLToken': token
|
|
66
|
+
},
|
|
67
|
+
body: postData
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
httpStatus = response.status;
|
|
71
|
+
resBody = await response.text();
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(resBody);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
parsed = { status: 'ERROR', error_message: 'Nieprawidłowa odpowiedź JSON z BaseLinker: ' + resBody.slice(0, 200) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const durationMs = Date.now() - start;
|
|
80
|
+
const ok = parsed && parsed.status === 'SUCCESS';
|
|
81
|
+
|
|
82
|
+
// Log do audytu
|
|
83
|
+
try {
|
|
84
|
+
logAudit({
|
|
85
|
+
service: 'baselinker',
|
|
86
|
+
method,
|
|
87
|
+
httpStatus,
|
|
88
|
+
durationMs,
|
|
89
|
+
ok,
|
|
90
|
+
details: ok ? 'OK' : (parsed.error_message || parsed.error_code)
|
|
91
|
+
});
|
|
92
|
+
} catch (_) {}
|
|
93
|
+
|
|
94
|
+
if (!ok) {
|
|
95
|
+
const msg = parsed.error_message || parsed.error_code || 'Nieznany błąd BaseLinker';
|
|
96
|
+
const err = new Error(`BaseLinker [${method}] błąd: ${msg}`);
|
|
97
|
+
err.errorCode = parsed.error_code;
|
|
98
|
+
err.raw = parsed;
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return parsed;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const durationMs = Date.now() - start;
|
|
105
|
+
try {
|
|
106
|
+
logAudit({
|
|
107
|
+
service: 'baselinker',
|
|
108
|
+
method,
|
|
109
|
+
httpStatus: httpStatus || 500,
|
|
110
|
+
durationMs,
|
|
111
|
+
ok: false,
|
|
112
|
+
details: err.message
|
|
113
|
+
});
|
|
114
|
+
} catch (_) {}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async testConnection() {
|
|
120
|
+
try {
|
|
121
|
+
const res = await this.call('getInventories');
|
|
122
|
+
return { ok: true, inventories: res.inventories || [] };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return { ok: false, error: err.message };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let defaultClient = null;
|
|
130
|
+
function getClient(token) {
|
|
131
|
+
if (token) return new BaseLinkerClient(token);
|
|
132
|
+
if (!defaultClient) defaultClient = new BaseLinkerClient();
|
|
133
|
+
return defaultClient;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { BaseLinkerClient, getClient, BASELINKER_API_URL };
|