@deerdesign/deertools 1.2.27 → 1.2.32
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 +55 -63
- package/package.json +4 -3
- package/src/baselinker-commands.js +21 -94
- package/src/cli.js +20 -6
- package/src/commands.js +167 -137
- package/src/completion.js +24 -18
- package/src/embedding.js +59 -0
- package/src/rendermind/cli-commands.js +31 -0
- package/src/rendermind/client.js +393 -0
- package/src/rendermind/mcp-server.js +305 -0
- package/src/skill-commands.js +157 -15
- package/src/skills-hub.js +66 -0
- package/src/supplier/cli-commands.js +32 -0
- package/src/supplier/mcp-server.js +360 -0
- package/src/tui.js +24 -39
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const credentials = require('../credentials');
|
|
5
|
+
const { getProduct } = require('../registry');
|
|
6
|
+
|
|
7
|
+
const RENDERMIND_API_BASE = 'https://lkljsuptgyktkwojnwbu.supabase.co/functions/v1/rendermind-api';
|
|
8
|
+
const CATALOG_API_BASE = 'https://ftzaobeixglnssemugjf.supabase.co/functions/v1/api-gateway';
|
|
9
|
+
const CATALOG_MCP_URL = 'https://ftzaobeixglnssemugjf.supabase.co/functions/v1/catalog-mcp';
|
|
10
|
+
const RENDERMIND_STORAGE_PREFIX = 'https://lkljsuptgyktkwojnwbu.supabase.co/storage/v1/object/public/ai-visualizations/';
|
|
11
|
+
|
|
12
|
+
function getRenderMindKey() {
|
|
13
|
+
const p = getProduct('rendermind');
|
|
14
|
+
const cred = credentials.getKey(p);
|
|
15
|
+
if (cred && cred.key) return cred.key;
|
|
16
|
+
if (process.env.RENDERMIND_API_KEY) return process.env.RENDERMIND_API_KEY;
|
|
17
|
+
if (process.env.DEERTOOLS_RENDERMIND_KEY) return process.env.DEERTOOLS_RENDERMIND_KEY;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getCatalogKey() {
|
|
22
|
+
const p = getProduct('catalog');
|
|
23
|
+
const cred = credentials.getKey(p);
|
|
24
|
+
if (cred && cred.key) return cred.key;
|
|
25
|
+
if (process.env.DEERTOOLS_CATALOG_KEY) return process.env.DEERTOOLS_CATALOG_KEY;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureRenderMindAuth() {
|
|
30
|
+
const key = getRenderMindKey();
|
|
31
|
+
if (!key) {
|
|
32
|
+
throw new Error('Brak klucza API do RenderMind. Zaloguj się poleceniem: deertools login rendermind --rw');
|
|
33
|
+
}
|
|
34
|
+
return key;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function callCatalogMcp(toolName, args) {
|
|
38
|
+
const catKey = getCatalogKey();
|
|
39
|
+
if (!catKey) return null;
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(CATALOG_MCP_URL, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'x-api-key': catKey,
|
|
46
|
+
'Authorization': `Bearer ${catKey}`,
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
jsonrpc: '2.0',
|
|
50
|
+
id: 1,
|
|
51
|
+
method: 'tools/call',
|
|
52
|
+
params: { name: toolName, arguments: args },
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) return null;
|
|
56
|
+
const json = await res.json();
|
|
57
|
+
const text = json?.result?.content?.[0]?.text;
|
|
58
|
+
if (!text) return null;
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
} catch (_) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function lookupProductBySku(skuOrUrl) {
|
|
66
|
+
if (!skuOrUrl) return null;
|
|
67
|
+
const raw = String(skuOrUrl).trim();
|
|
68
|
+
const catKey = getCatalogKey();
|
|
69
|
+
const headers = { 'Accept': 'application/json' };
|
|
70
|
+
if (catKey) headers['x-api-key'] = catKey;
|
|
71
|
+
|
|
72
|
+
// 1. Sprawdź czy to URL produktu lub slug z Katalogu Connect
|
|
73
|
+
const isUrl = /^https?:\/\//i.test(raw) || raw.includes('/katalog/product/');
|
|
74
|
+
if (isUrl) {
|
|
75
|
+
const slugMatch = raw.match(/\/product\/([^/?#]+)/i);
|
|
76
|
+
const slug = slugMatch ? slugMatch[1] : raw;
|
|
77
|
+
const extMatch = slug.match(/-(\d{5,})$/);
|
|
78
|
+
const extId = extMatch ? extMatch[1] : null;
|
|
79
|
+
const words = slug
|
|
80
|
+
.replace(/-\d{5,}$/, '')
|
|
81
|
+
.split('-')
|
|
82
|
+
.filter((w) => w.length >= 3)
|
|
83
|
+
.slice(0, 4)
|
|
84
|
+
.join(' ');
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const searchQ = words || extId || slug;
|
|
88
|
+
const res = await fetch(`${CATALOG_API_BASE}/products?search=${encodeURIComponent(searchQ)}&active=all&limit=25`, { headers });
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
const list = data?.data || data?.items || [];
|
|
92
|
+
const matched = list.find((p) => p.slug === slug || (extId && String(p.external_id) === extId)) || list[0];
|
|
93
|
+
if (matched && matched.id) {
|
|
94
|
+
const full = await callCatalogMcp('catalog_get_product', { id: matched.id });
|
|
95
|
+
return full || matched;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (_) {}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Spróbuj bezpośrednio przez catalog_get_product po SKU
|
|
102
|
+
const direct = await callCatalogMcp('catalog_get_product', { sku: raw });
|
|
103
|
+
if (direct && (direct.id || direct.sku)) return direct;
|
|
104
|
+
|
|
105
|
+
if (raw.includes('|')) {
|
|
106
|
+
const firstPart = raw.split('|')[0].trim();
|
|
107
|
+
const partDirect = await callCatalogMcp('catalog_get_product', { sku: firstPart });
|
|
108
|
+
if (partDirect && (partDirect.id || partDirect.sku)) return partDirect;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Fallback: wyszukiwanie przez search=...&active=all
|
|
112
|
+
try {
|
|
113
|
+
const q = raw.includes('|') ? raw.split('|')[0].trim() : raw;
|
|
114
|
+
const res = await fetch(`${CATALOG_API_BASE}/products?search=${encodeURIComponent(q)}&active=all&limit=10`, { headers });
|
|
115
|
+
if (res.ok) {
|
|
116
|
+
const data = await res.json();
|
|
117
|
+
const list = data?.data || data?.items || [];
|
|
118
|
+
const exact = list.find((p) => p.sku === raw || p.sku === q || String(p.external_id) === q) || list[0];
|
|
119
|
+
if (exact) return exact;
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function uploadImage({ filePath, buffer, fileName, contentType }) {
|
|
127
|
+
const apiKey = ensureRenderMindAuth();
|
|
128
|
+
let bytes;
|
|
129
|
+
let finalFileName = fileName;
|
|
130
|
+
let finalContentType = contentType;
|
|
131
|
+
|
|
132
|
+
if (filePath) {
|
|
133
|
+
if (!fs.existsSync(filePath)) throw new Error(`Plik nie istnieje: ${filePath}`);
|
|
134
|
+
bytes = fs.readFileSync(filePath);
|
|
135
|
+
if (!finalFileName) finalFileName = path.basename(filePath);
|
|
136
|
+
if (!finalContentType) {
|
|
137
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
138
|
+
if (ext === '.jpg' || ext === '.jpeg') finalContentType = 'image/jpeg';
|
|
139
|
+
else if (ext === '.png') finalContentType = 'image/png';
|
|
140
|
+
else if (ext === '.webp') finalContentType = 'image/webp';
|
|
141
|
+
else finalContentType = 'image/png';
|
|
142
|
+
}
|
|
143
|
+
} else if (buffer) {
|
|
144
|
+
bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
145
|
+
if (!finalFileName) finalFileName = 'upload.png';
|
|
146
|
+
if (!finalContentType) finalContentType = 'image/png';
|
|
147
|
+
} else {
|
|
148
|
+
throw new Error('Wymagany parametr: filePath lub buffer');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 1. Zgłoszenie żądania podpisanego URL-a uploadu
|
|
152
|
+
const uploadRes = await fetch(`${RENDERMIND_API_BASE}/uploads`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: {
|
|
155
|
+
'x-api-key': apiKey,
|
|
156
|
+
'Content-Type': 'application/json'
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
file_name: finalFileName,
|
|
160
|
+
content_type: finalContentType,
|
|
161
|
+
size_bytes: bytes.length
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const uploadData = await uploadRes.json();
|
|
166
|
+
if (!uploadRes.ok) {
|
|
167
|
+
throw new Error(`Błąd pobierania upload URL (${uploadRes.status}): ${uploadData.error || JSON.stringify(uploadData)}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 2. Wgranie bajtów pod podpisany adres Supabase Storage
|
|
171
|
+
const putRes = await fetch(uploadData.upload_url, {
|
|
172
|
+
method: 'PUT',
|
|
173
|
+
headers: { 'Content-Type': finalContentType },
|
|
174
|
+
body: bytes
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!putRes.ok) {
|
|
178
|
+
throw new Error(`Błąd zapisu pliku do storage (${putRes.status}): ${putRes.statusText}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
public_url: uploadData.public_url,
|
|
183
|
+
path: uploadData.path,
|
|
184
|
+
size_bytes: bytes.length,
|
|
185
|
+
file_name: finalFileName
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function ensureOwnedRenderMindUrl(url) {
|
|
190
|
+
if (!url) return null;
|
|
191
|
+
if (url.startsWith(RENDERMIND_STORAGE_PREFIX)) return url;
|
|
192
|
+
|
|
193
|
+
const res = await fetch(url);
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
throw new Error(`Nie udało się pobrać obrazu źródłowego z URL (${res.status}): ${url}`);
|
|
196
|
+
}
|
|
197
|
+
const contentTypeHeader = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
198
|
+
let contentType = 'image/jpeg';
|
|
199
|
+
let ext = '.jpg';
|
|
200
|
+
if (contentTypeHeader === 'image/png' || url.toLowerCase().endsWith('.png')) {
|
|
201
|
+
contentType = 'image/png';
|
|
202
|
+
ext = '.png';
|
|
203
|
+
} else if (contentTypeHeader === 'image/webp' || url.toLowerCase().endsWith('.webp')) {
|
|
204
|
+
contentType = 'image/webp';
|
|
205
|
+
ext = '.webp';
|
|
206
|
+
}
|
|
207
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
208
|
+
const uploaded = await uploadImage({
|
|
209
|
+
buffer: buf,
|
|
210
|
+
fileName: `product_ref${ext}`,
|
|
211
|
+
contentType,
|
|
212
|
+
});
|
|
213
|
+
return uploaded.public_url;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function createVisualization({ prompt, imageUrls = [], imagePath = null, sku = null, productUrl = null, kind = 'photo' }) {
|
|
217
|
+
const apiKey = ensureRenderMindAuth();
|
|
218
|
+
let rawUrls = [...(imageUrls || [])];
|
|
219
|
+
|
|
220
|
+
if (imagePath && rawUrls.length === 0) {
|
|
221
|
+
const uploadRes = await uploadImage({ filePath: imagePath });
|
|
222
|
+
rawUrls.push(uploadRes.public_url);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let enrichedPrompt = prompt || '';
|
|
226
|
+
let productInfo = null;
|
|
227
|
+
const lookupQuery = sku || productUrl;
|
|
228
|
+
|
|
229
|
+
if (lookupQuery) {
|
|
230
|
+
productInfo = await lookupProductBySku(lookupQuery);
|
|
231
|
+
if (productInfo) {
|
|
232
|
+
const details = [];
|
|
233
|
+
if (productInfo.name) details.push(`Produkt: ${productInfo.name}`);
|
|
234
|
+
if (productInfo.features && Object.keys(productInfo.features).length) {
|
|
235
|
+
details.push(`Cechy: ${JSON.stringify(productInfo.features)}`);
|
|
236
|
+
}
|
|
237
|
+
if (productInfo.tags && Array.isArray(productInfo.tags)) {
|
|
238
|
+
const pkg = productInfo.tags.find((t) => ['SMART', 'MODERN', 'HIGH'].includes(String(t).toUpperCase()));
|
|
239
|
+
if (pkg) details.push(`Pakiet: ${pkg}`);
|
|
240
|
+
} else if (productInfo.finish_package) {
|
|
241
|
+
details.push(`Pakiet: ${productInfo.finish_package}`);
|
|
242
|
+
}
|
|
243
|
+
const resolvedSku = productInfo.sku || sku || '';
|
|
244
|
+
enrichedPrompt = `${enrichedPrompt ? enrichedPrompt + '. ' : ''}Użyj i wiernie odwzoruj produkt w aranżacji wnętrza [SKU: ${resolvedSku}]: ${details.join(', ')}. Zachowaj geometrię, proporcje, wykończenie i detale materiałowe.`;
|
|
245
|
+
|
|
246
|
+
// Jeśli nie podano żadnego obrazu, automatycznie użyj zdjęcia referencyjnego produktu z Katalogu Connect
|
|
247
|
+
if (rawUrls.length === 0 && Array.isArray(productInfo.images) && productInfo.images.length > 0) {
|
|
248
|
+
const preferredImg = productInfo.images.find((u) => u.includes('/mirror/')) || productInfo.images[0];
|
|
249
|
+
rawUrls.push(preferredImg);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (rawUrls.length === 0) {
|
|
255
|
+
throw new Error('RenderMind wymaga co najmniej jednego obrazu bazowego (parametr image_path, image_url lub sku/product_url produktu ze zdjęciem w Katalogu Connect).');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Upewnij się, że każdy obraz znajduje się w autoryzowanym buckecie RenderMind
|
|
259
|
+
const finalUrls = [];
|
|
260
|
+
for (const u of rawUrls.slice(0, 1)) {
|
|
261
|
+
const ownedUrl = await ensureOwnedRenderMindUrl(u);
|
|
262
|
+
if (ownedUrl) finalUrls.push(ownedUrl);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const res = await fetch(`${RENDERMIND_API_BASE}/visualizations`, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: {
|
|
268
|
+
'x-api-key': apiKey,
|
|
269
|
+
'Content-Type': 'application/json'
|
|
270
|
+
},
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
image_urls: finalUrls,
|
|
273
|
+
prompt: enrichedPrompt,
|
|
274
|
+
kind: kind || 'photo'
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const data = await res.json();
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
throw new Error(`Błąd tworzenia wizualizacji (${res.status}): ${data.error || JSON.stringify(data)}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
job_id: data.job_id,
|
|
285
|
+
conversation_id: data.conversation_id,
|
|
286
|
+
prompt: enrichedPrompt,
|
|
287
|
+
input_image_urls: finalUrls,
|
|
288
|
+
product: productInfo ? {
|
|
289
|
+
id: productInfo.id,
|
|
290
|
+
sku: productInfo.sku || sku,
|
|
291
|
+
name: productInfo.name,
|
|
292
|
+
url: productInfo.url,
|
|
293
|
+
images: productInfo.images
|
|
294
|
+
} : null
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function getVisualization(jobId) {
|
|
299
|
+
const apiKey = ensureRenderMindAuth();
|
|
300
|
+
const res = await fetch(`${RENDERMIND_API_BASE}/visualizations/${jobId}`, {
|
|
301
|
+
headers: { 'x-api-key': apiKey }
|
|
302
|
+
});
|
|
303
|
+
const data = await res.json();
|
|
304
|
+
if (!res.ok) {
|
|
305
|
+
throw new Error(`Błąd pobierania statusu wizualizacji (${res.status}): ${data.error || JSON.stringify(data)}`);
|
|
306
|
+
}
|
|
307
|
+
return data;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function getMe() {
|
|
311
|
+
const apiKey = ensureRenderMindAuth();
|
|
312
|
+
const res = await fetch(`${RENDERMIND_API_BASE}/me`, {
|
|
313
|
+
headers: { 'x-api-key': apiKey }
|
|
314
|
+
});
|
|
315
|
+
const data = await res.json();
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
throw new Error(`Błąd sprawdzania uprawnień RenderMind (${res.status}): ${data.error || JSON.stringify(data)}`);
|
|
318
|
+
}
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function pollVisualization(jobId, { timeoutMs = 180000, intervalMs = 2500, onProgress = null } = {}) {
|
|
323
|
+
const startTime = Date.now();
|
|
324
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
325
|
+
const data = await getVisualization(jobId);
|
|
326
|
+
if (onProgress) onProgress(data);
|
|
327
|
+
if (data.status === 'done' || data.status === 'completed') {
|
|
328
|
+
return {
|
|
329
|
+
success: true,
|
|
330
|
+
job_id: data.job_id,
|
|
331
|
+
conversation_id: data.conversation_id,
|
|
332
|
+
status: 'completed',
|
|
333
|
+
image_urls: data.output_image_urls || [],
|
|
334
|
+
error: null
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (data.status === 'failed' || data.error) {
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
job_id: data.job_id,
|
|
341
|
+
status: 'failed',
|
|
342
|
+
error: data.error || 'Nieznany błąd silnika generowania wizualizacji'
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
346
|
+
}
|
|
347
|
+
throw new Error(`Upłynął limit czasu (${Math.round(timeoutMs / 1000)}s) na ukończenie wizualizacji.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function estimateFloorPlan({ fileUrl, filePath, rooms, kitchenOpenAreaM2 = 6 }) {
|
|
351
|
+
const apiKey = ensureRenderMindAuth();
|
|
352
|
+
let finalFileUrl = fileUrl;
|
|
353
|
+
|
|
354
|
+
if (filePath && !finalFileUrl) {
|
|
355
|
+
const uploadRes = await uploadImage({ filePath });
|
|
356
|
+
finalFileUrl = uploadRes.public_url;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const payload = {
|
|
360
|
+
kitchen_open_area_m2: kitchenOpenAreaM2
|
|
361
|
+
};
|
|
362
|
+
if (finalFileUrl) payload.file_url = finalFileUrl;
|
|
363
|
+
if (rooms && Array.isArray(rooms)) payload.rooms = rooms;
|
|
364
|
+
|
|
365
|
+
const res = await fetch(`${RENDERMIND_API_BASE}/floor-plan-estimates`, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: {
|
|
368
|
+
'x-api-key': apiKey,
|
|
369
|
+
'Content-Type': 'application/json'
|
|
370
|
+
},
|
|
371
|
+
body: JSON.stringify(payload)
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const data = await res.json();
|
|
375
|
+
if (!res.ok) {
|
|
376
|
+
throw new Error(`Błąd szacowania rzutu (${res.status}): ${data.error || JSON.stringify(data)}`);
|
|
377
|
+
}
|
|
378
|
+
return data;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = {
|
|
382
|
+
getRenderMindKey,
|
|
383
|
+
getCatalogKey,
|
|
384
|
+
ensureRenderMindAuth,
|
|
385
|
+
lookupProductBySku,
|
|
386
|
+
uploadImage,
|
|
387
|
+
ensureOwnedRenderMindUrl,
|
|
388
|
+
createVisualization,
|
|
389
|
+
getVisualization,
|
|
390
|
+
getMe,
|
|
391
|
+
pollVisualization,
|
|
392
|
+
estimateFloorPlan
|
|
393
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const client = require('./client');
|
|
4
|
+
|
|
5
|
+
const PROTOCOL_VERSION = '2024-11-05';
|
|
6
|
+
const SERVER_INFO = { name: 'deer-rendermind-mcp', version: '1.1.0' };
|
|
7
|
+
|
|
8
|
+
const RENDERMIND_TOOLS = [
|
|
9
|
+
{
|
|
10
|
+
name: 'rendermind_create_visualization',
|
|
11
|
+
description: 'Generuje fotorealistyczną wizualizację wnętrza w RenderMind Deer Design. Przyjmuje kod SKU lub link do produktu z Katalogu Connect, opcjonalną ścieżkę lokalną do szkicu/rzutu lub adres URL oraz prompt stylistyczny. Automatycznie dociąga specyfikację i zdjęcie produktu z Katalogu Connect, wgrywa obraz do bucketu RenderMind i oczekuje na ukończenie renderu.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
prompt: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Opis stylu wnętrza, oświetlenia, kolorystyki, np. "Elegancka łazienka w pakiecie High Life, płytki wielkoformatowe trawertyn, armatura złoty szczotkowany, ciepłe światło 3000K".'
|
|
18
|
+
},
|
|
19
|
+
sku: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'Kod SKU produktu z bazy Katalog Connect (np. A5D186ENB0, NAC_09LP, BQA_R40M|NQA_R51K) lub URL produktu. Narzędzie automatycznie pobierze specyfikację i zdjęcie produktu z Katalogu Connect.'
|
|
22
|
+
},
|
|
23
|
+
product_url: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Opcjonalny pełny adres URL karty produktu z Katalogu Connect (np. https://connect.deerdesign.pl/katalog/product/...).'
|
|
26
|
+
},
|
|
27
|
+
image_path: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Opcjonalna lokalna ścieżka do pliku rzutu, szkicu pCon lub zdjęcia referencyjnego (jeśli brak, zostanie użyte zdjęcie produktu z Katalogu Connect dla podanego SKU).'
|
|
30
|
+
},
|
|
31
|
+
image_url: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Opcjonalny adres URL do obrazu bazowego (jeśli pochodzi spoza RenderMind, zostanie automatycznie pobrany i wgrany do bucketu RenderMind).'
|
|
34
|
+
},
|
|
35
|
+
kind: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
enum: ['pcon', 'photo'],
|
|
38
|
+
default: 'photo',
|
|
39
|
+
description: 'Typ wejścia: "pcon" dla szkicu technicznego CAD/pCon, "photo" dla zdjęcia lub rzutu.'
|
|
40
|
+
},
|
|
41
|
+
wait: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
default: true,
|
|
44
|
+
description: 'Czy czekać na wygenerowanie gotowego renderu (domyślnie true).'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
required: ['prompt']
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'rendermind_get_visualization',
|
|
52
|
+
description: 'Pobiera aktualny status zadania wizualizacji w RenderMind po identyfikatorze job_id.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
job_id: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Identyfikator zadania wizualizacji (UUID).'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
required: ['job_id']
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'rendermind_upload_image',
|
|
66
|
+
description: 'Wgrywa lokalny plik graficzny (PNG, JPG, WEBP) do dedykowanego magazynu RenderMind i zwraca publiczny URL do użycia w wizualizacjach i wycenach.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
file_path: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Bezwzględna lub względna ścieżka do lokalnego pliku obrazu.'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
required: ['file_path']
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'rendermind_floor_plan_estimate',
|
|
80
|
+
description: 'Szacuje zestawienie powierzchni i koszty wykończenia mieszkania z rzutu architektonicznego lub listy pomieszczeń w RenderMind.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
image_path: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Ścieżka do pliku rzutu architektonicznego na dysku.'
|
|
87
|
+
},
|
|
88
|
+
file_url: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Publiczny URL do pliku rzutu.'
|
|
91
|
+
},
|
|
92
|
+
kitchen_open_area_m2: {
|
|
93
|
+
type: 'number',
|
|
94
|
+
default: 6,
|
|
95
|
+
description: 'Szacowany metraż otwartego aneksu kuchennego w m2 (domyślnie 6 m2).'
|
|
96
|
+
},
|
|
97
|
+
rooms: {
|
|
98
|
+
type: 'array',
|
|
99
|
+
description: 'Opcjonalna lista pomieszczeń z metrażami.',
|
|
100
|
+
items: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
name: { type: 'string' },
|
|
104
|
+
area_m2: { type: 'number' },
|
|
105
|
+
perimeter_m: { type: 'number' }
|
|
106
|
+
},
|
|
107
|
+
required: ['name', 'area_m2']
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
async function handleMcpMessage(msg) {
|
|
116
|
+
if (!msg || typeof msg !== 'object') return null;
|
|
117
|
+
const { id, method, params } = msg;
|
|
118
|
+
|
|
119
|
+
if (method === 'initialize') {
|
|
120
|
+
return {
|
|
121
|
+
jsonrpc: '2.0',
|
|
122
|
+
id,
|
|
123
|
+
result: {
|
|
124
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
125
|
+
capabilities: { tools: {} },
|
|
126
|
+
serverInfo: SERVER_INFO
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (method === 'notifications/initialized' || method === 'initialized') {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (method === 'ping') {
|
|
136
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (method === 'tools/list') {
|
|
140
|
+
return {
|
|
141
|
+
jsonrpc: '2.0',
|
|
142
|
+
id,
|
|
143
|
+
result: {
|
|
144
|
+
tools: RENDERMIND_TOOLS
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (method === 'tools/call') {
|
|
150
|
+
const toolName = params?.name;
|
|
151
|
+
const args = params?.arguments || {};
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
if (toolName === 'rendermind_create_visualization') {
|
|
155
|
+
const imageUrls = [];
|
|
156
|
+
if (args.image_url) imageUrls.push(args.image_url);
|
|
157
|
+
|
|
158
|
+
const initRes = await client.createVisualization({
|
|
159
|
+
prompt: args.prompt,
|
|
160
|
+
imageUrls,
|
|
161
|
+
imagePath: args.image_path,
|
|
162
|
+
sku: args.sku,
|
|
163
|
+
productUrl: args.product_url,
|
|
164
|
+
kind: args.kind || 'photo'
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (args.wait === false) {
|
|
168
|
+
return {
|
|
169
|
+
jsonrpc: '2.0',
|
|
170
|
+
id,
|
|
171
|
+
result: {
|
|
172
|
+
content: [{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: JSON.stringify({
|
|
175
|
+
status: 'running',
|
|
176
|
+
job_id: initRes.job_id,
|
|
177
|
+
conversation_id: initRes.conversation_id,
|
|
178
|
+
prompt: initRes.prompt,
|
|
179
|
+
product: initRes.product
|
|
180
|
+
}, null, 2)
|
|
181
|
+
}]
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pollRes = await client.pollVisualization(initRes.job_id, {
|
|
187
|
+
timeoutMs: 180000,
|
|
188
|
+
intervalMs: 2500
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
jsonrpc: '2.0',
|
|
193
|
+
id,
|
|
194
|
+
result: {
|
|
195
|
+
content: [{
|
|
196
|
+
type: 'text',
|
|
197
|
+
text: JSON.stringify({
|
|
198
|
+
status: pollRes.status,
|
|
199
|
+
job_id: pollRes.job_id,
|
|
200
|
+
conversation_id: pollRes.conversation_id,
|
|
201
|
+
image_urls: pollRes.image_urls,
|
|
202
|
+
prompt: initRes.prompt,
|
|
203
|
+
product: initRes.product
|
|
204
|
+
}, null, 2)
|
|
205
|
+
}]
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (toolName === 'rendermind_get_visualization') {
|
|
211
|
+
const data = await client.getVisualization(args.job_id);
|
|
212
|
+
return {
|
|
213
|
+
jsonrpc: '2.0',
|
|
214
|
+
id,
|
|
215
|
+
result: {
|
|
216
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (toolName === 'rendermind_upload_image') {
|
|
222
|
+
const data = await client.uploadImage({ filePath: args.file_path });
|
|
223
|
+
return {
|
|
224
|
+
jsonrpc: '2.0',
|
|
225
|
+
id,
|
|
226
|
+
result: {
|
|
227
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (toolName === 'rendermind_floor_plan_estimate') {
|
|
233
|
+
const data = await client.estimateFloorPlan({
|
|
234
|
+
filePath: args.image_path,
|
|
235
|
+
fileUrl: args.file_url,
|
|
236
|
+
rooms: args.rooms,
|
|
237
|
+
kitchenOpenAreaM2: args.kitchen_open_area_m2
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
jsonrpc: '2.0',
|
|
241
|
+
id,
|
|
242
|
+
result: {
|
|
243
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
jsonrpc: '2.0',
|
|
250
|
+
id,
|
|
251
|
+
error: { code: -32601, message: `Nieznane narzędzie: ${toolName}` }
|
|
252
|
+
};
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return {
|
|
255
|
+
jsonrpc: '2.0',
|
|
256
|
+
id,
|
|
257
|
+
error: { code: -32000, message: e.message }
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
jsonrpc: '2.0',
|
|
264
|
+
id,
|
|
265
|
+
error: { code: -32601, message: `Nieobsługiwana metoda: ${method}` }
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runRenderMindMcpServer() {
|
|
270
|
+
return new Promise((resolve) => {
|
|
271
|
+
const rl = readline.createInterface({
|
|
272
|
+
input: process.stdin,
|
|
273
|
+
output: process.stdout,
|
|
274
|
+
terminal: false
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
rl.on('line', async (line) => {
|
|
278
|
+
const trimmed = line.trim();
|
|
279
|
+
if (!trimmed) return;
|
|
280
|
+
try {
|
|
281
|
+
const msg = JSON.parse(trimmed);
|
|
282
|
+
const resp = await handleMcpMessage(msg);
|
|
283
|
+
if (resp) {
|
|
284
|
+
process.stdout.write(JSON.stringify(resp) + '\n');
|
|
285
|
+
}
|
|
286
|
+
} catch (e) {
|
|
287
|
+
process.stdout.write(JSON.stringify({
|
|
288
|
+
jsonrpc: '2.0',
|
|
289
|
+
id: null,
|
|
290
|
+
error: { code: -32700, message: `Parse error: ${e.message}` }
|
|
291
|
+
}) + '\n');
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
rl.on('close', () => resolve());
|
|
296
|
+
process.on('SIGINT', () => { rl.close(); resolve(); });
|
|
297
|
+
process.on('SIGTERM', () => { rl.close(); resolve(); });
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
RENDERMIND_TOOLS,
|
|
303
|
+
handleMcpMessage,
|
|
304
|
+
runRenderMindMcpServer
|
|
305
|
+
};
|