n8n-nodes-comfyui-all 2.2.9 → 2.2.11

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.
Files changed (57) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +126 -11
  3. package/dist/agent-tools/ComfyUIAgentTool.d.ts +62 -0
  4. package/dist/agent-tools/ComfyUIAgentTool.d.ts.map +1 -0
  5. package/dist/agent-tools/ComfyUIAgentTool.js +440 -0
  6. package/dist/agent-tools/ComfyUIAgentTool.js.map +1 -0
  7. package/dist/nodes/AxiosAdapter.d.ts +5 -0
  8. package/dist/nodes/AxiosAdapter.d.ts.map +1 -0
  9. package/dist/nodes/AxiosAdapter.js +31 -0
  10. package/dist/nodes/AxiosAdapter.js.map +1 -0
  11. package/dist/nodes/ComfyUi/ComfyUi.node.d.ts +30 -0
  12. package/dist/nodes/ComfyUi/ComfyUi.node.d.ts.map +1 -1
  13. package/dist/nodes/ComfyUi/ComfyUi.node.js +47 -251
  14. package/dist/nodes/ComfyUi/ComfyUi.node.js.map +1 -1
  15. package/dist/nodes/ComfyUiClient.d.ts +116 -3
  16. package/dist/nodes/ComfyUiClient.d.ts.map +1 -1
  17. package/dist/nodes/ComfyUiClient.js +374 -98
  18. package/dist/nodes/ComfyUiClient.js.map +1 -1
  19. package/dist/nodes/HttpClient.d.ts +34 -0
  20. package/dist/nodes/HttpClient.d.ts.map +1 -0
  21. package/dist/nodes/HttpClient.js +85 -0
  22. package/dist/nodes/HttpClient.js.map +1 -0
  23. package/dist/nodes/N8nHelpersAdapter.d.ts +8 -0
  24. package/dist/nodes/N8nHelpersAdapter.d.ts.map +1 -0
  25. package/dist/nodes/N8nHelpersAdapter.js +23 -0
  26. package/dist/nodes/N8nHelpersAdapter.js.map +1 -0
  27. package/dist/nodes/constants.d.ts +14 -3
  28. package/dist/nodes/constants.d.ts.map +1 -1
  29. package/dist/nodes/constants.js +20 -14
  30. package/dist/nodes/constants.js.map +1 -1
  31. package/dist/nodes/errors.d.ts +35 -0
  32. package/dist/nodes/errors.d.ts.map +1 -0
  33. package/dist/nodes/errors.js +111 -0
  34. package/dist/nodes/errors.js.map +1 -0
  35. package/dist/nodes/logger.d.ts +9 -17
  36. package/dist/nodes/logger.d.ts.map +1 -1
  37. package/dist/nodes/logger.js +72 -23
  38. package/dist/nodes/logger.js.map +1 -1
  39. package/dist/nodes/parameterProcessor.d.ts +22 -0
  40. package/dist/nodes/parameterProcessor.d.ts.map +1 -0
  41. package/dist/nodes/parameterProcessor.js +268 -0
  42. package/dist/nodes/parameterProcessor.js.map +1 -0
  43. package/dist/nodes/types.d.ts +129 -0
  44. package/dist/nodes/types.d.ts.map +1 -1
  45. package/dist/nodes/utils.d.ts +65 -0
  46. package/dist/nodes/utils.d.ts.map +1 -0
  47. package/dist/nodes/utils.js +172 -0
  48. package/dist/nodes/utils.js.map +1 -0
  49. package/dist/nodes/validation.d.ts +8 -0
  50. package/dist/nodes/validation.d.ts.map +1 -1
  51. package/dist/nodes/validation.js +129 -7
  52. package/dist/nodes/validation.js.map +1 -1
  53. package/dist/nodes/workflowConfig.d.ts +13 -0
  54. package/dist/nodes/workflowConfig.d.ts.map +1 -0
  55. package/dist/nodes/workflowConfig.js +91 -0
  56. package/dist/nodes/workflowConfig.js.map +1 -0
  57. package/package.json +18 -2
@@ -7,12 +7,30 @@ exports.ComfyUIClient = void 0;
7
7
  const crypto_1 = require("crypto");
8
8
  const constants_1 = require("./constants");
9
9
  const form_data_1 = __importDefault(require("form-data"));
10
+ const logger_1 = require("./logger");
11
+ const utils_1 = require("./utils");
12
+ const HttpClient_1 = require("./HttpClient");
13
+ const N8nHelpersAdapter_1 = require("./N8nHelpersAdapter");
14
+ /**
15
+ * Client state enumeration for state machine pattern
16
+ */
17
+ var ClientState;
18
+ (function (ClientState) {
19
+ ClientState["IDLE"] = "idle";
20
+ ClientState["REQUESTING"] = "requesting";
21
+ ClientState["DESTROYED"] = "destroyed";
22
+ })(ClientState || (ClientState = {}));
10
23
  class ComfyUIClient {
11
24
  constructor(config) {
12
- this.isDestroyed = false;
13
- this.helpers = config.helpers;
25
+ this.state = ClientState.IDLE;
26
+ this.currentAbortController = null;
27
+ this.httpClient = new HttpClient_1.HttpClient({
28
+ adapter: new N8nHelpersAdapter_1.N8nHelpersAdapter(config.helpers),
29
+ logger: config.logger || new logger_1.Logger(),
30
+ defaultTimeout: config.timeout || constants_1.VALIDATION.REQUEST_TIMEOUT_MS,
31
+ });
32
+ this.logger = config.logger || new logger_1.Logger();
14
33
  this.baseUrl = config.baseUrl;
15
- this.timeout = config.timeout || constants_1.VALIDATION.REQUEST_TIMEOUT_MS;
16
34
  this.clientId = config.clientId || this.generateClientId();
17
35
  this.maxRetries = config.maxRetries ?? constants_1.VALIDATION.MAX_RETRIES;
18
36
  }
@@ -20,45 +38,100 @@ class ComfyUIClient {
20
38
  * Cancel any ongoing request and clean up resources
21
39
  */
22
40
  cancelRequest() {
23
- this.isDestroyed = true;
41
+ if (this.currentAbortController) {
42
+ this.currentAbortController.abort();
43
+ this.currentAbortController = null;
44
+ }
24
45
  }
25
46
  /**
26
47
  * Clean up resources when the client is no longer needed
27
48
  */
28
49
  destroy() {
50
+ this.state = ClientState.DESTROYED;
29
51
  this.cancelRequest();
30
52
  }
31
53
  /**
32
54
  * Check if the client has been destroyed
33
55
  */
34
56
  isClientDestroyed() {
35
- return this.isDestroyed;
57
+ return this.state === ClientState.DESTROYED;
36
58
  }
59
+ /**
60
+ * Generate a unique client ID
61
+ * @returns Unique client ID string
62
+ */
37
63
  generateClientId() {
38
64
  return `client_${(0, crypto_1.randomUUID)()}`;
39
65
  }
66
+ /**
67
+ * Delay execution for a specified time
68
+ * @param ms - Delay time in milliseconds
69
+ * @returns Promise that resolves after the delay
70
+ */
71
+ async delay(ms) {
72
+ return new Promise(resolve => setTimeout(resolve, ms));
73
+ }
74
+ /**
75
+ * Retry a request with exponential backoff
76
+ * @param requestFn - Function that returns a Promise to retry
77
+ * @param retries - Maximum number of retry attempts
78
+ * @returns Promise that resolves when the request succeeds
79
+ * @throws Error if all retry attempts fail
80
+ */
40
81
  async retryRequest(requestFn, retries = this.maxRetries) {
82
+ // Check if client is destroyed before starting
83
+ if (this.state === ClientState.DESTROYED) {
84
+ throw new Error('Client has been destroyed');
85
+ }
41
86
  let lastError;
42
- for (let attempt = 0; attempt <= retries; attempt++) {
43
- try {
44
- if (this.isDestroyed) {
45
- throw new Error('Client has been destroyed');
87
+ const baseDelay = constants_1.VALIDATION.RETRY_DELAY_MS;
88
+ const maxBackoffDelay = constants_1.VALIDATION.MAX_BACKOFF_DELAY_RETRY;
89
+ let backoffDelay = baseDelay;
90
+ // Set state to REQUESTING and create AbortController
91
+ this.state = ClientState.REQUESTING;
92
+ this.currentAbortController = new AbortController();
93
+ try {
94
+ for (let attempt = 0; attempt <= retries; attempt++) {
95
+ try {
96
+ // Check if request was aborted
97
+ if (this.currentAbortController.signal.aborted) {
98
+ throw new Error('Request aborted');
99
+ }
100
+ return await requestFn();
46
101
  }
47
- return await requestFn();
48
- }
49
- catch (error) {
50
- lastError = error;
51
- if (attempt < retries) {
52
- // Use a microtask to yield control between retries
53
- await Promise.resolve();
102
+ catch (error) {
103
+ lastError = error;
104
+ // If aborted, don't retry
105
+ if (error instanceof Error && (error.name === 'AbortError' || error.message === 'Request aborted')) {
106
+ throw new Error('Request was cancelled');
107
+ }
108
+ if (attempt < retries) {
109
+ // Exponential backoff: double the delay with each error, capped at maxBackoffDelay
110
+ backoffDelay = Math.min(backoffDelay * 2, maxBackoffDelay);
111
+ if (attempt > 0) {
112
+ const errorMsg = error instanceof Error ? error.message : String(error);
113
+ this.logger.warn(`Request attempt ${attempt + 1}/${retries + 1} failed, retrying in ${backoffDelay}ms: ${errorMsg}`);
114
+ }
115
+ // Wait before retrying with exponential backoff
116
+ await this.delay(backoffDelay);
117
+ }
54
118
  }
55
119
  }
120
+ throw lastError;
121
+ }
122
+ finally {
123
+ // Reset state to IDLE after request completes
124
+ this.state = ClientState.IDLE;
56
125
  }
57
- throw lastError;
58
126
  }
127
+ /**
128
+ * Execute a ComfyUI workflow
129
+ * @param workflow - Workflow object containing nodes and their configurations
130
+ * @returns Promise containing workflow execution result
131
+ */
59
132
  async executeWorkflow(workflow) {
60
133
  try {
61
- if (this.isDestroyed) {
134
+ if (this.isClientDestroyed()) {
62
135
  return {
63
136
  success: false,
64
137
  error: 'Client has been destroyed',
@@ -69,16 +142,12 @@ class ComfyUIClient {
69
142
  prompt,
70
143
  client_id: this.clientId,
71
144
  };
72
- // Debug: Log the request body
73
- console.log('[ComfyUI] Sending workflow to ComfyUI:', JSON.stringify(requestBody, null, 2));
74
- const response = await this.retryRequest(() => this.helpers.httpRequest({
75
- method: 'POST',
76
- url: `${this.baseUrl}/prompt`,
145
+ this.logger.debug('Sending workflow to ComfyUI:', JSON.stringify(requestBody, null, 2));
146
+ const response = await this.retryRequest(() => this.httpClient.post(`${this.baseUrl}/prompt`, requestBody, {
77
147
  json: true,
78
- body: requestBody,
79
- timeout: this.timeout,
148
+ abortSignal: this.currentAbortController?.signal,
80
149
  }));
81
- console.log('[ComfyUI] Response from ComfyUI:', JSON.stringify(response, null, 2));
150
+ this.logger.debug('Response from ComfyUI:', JSON.stringify(response, null, 2));
82
151
  if (response.prompt_id) {
83
152
  return await this.waitForExecution(response.prompt_id);
84
153
  }
@@ -88,12 +157,14 @@ class ComfyUIClient {
88
157
  };
89
158
  }
90
159
  catch (error) {
91
- console.error('[ComfyUI] Workflow execution error:', error);
92
- console.error('[ComfyUI] Error details:', {
93
- message: error.message,
94
- statusCode: error.response?.statusCode || error.statusCode,
95
- statusMessage: error.response?.statusMessage || error.statusMessage,
96
- responseBody: error.response?.body || error.response?.data,
160
+ this.logger.error('Workflow execution error:', error);
161
+ const err = error instanceof Error ? error : new Error(String(error));
162
+ const httpError = err;
163
+ this.logger.error('Error details:', {
164
+ message: err.message,
165
+ statusCode: httpError.response?.statusCode || httpError.statusCode,
166
+ statusMessage: httpError.response?.statusMessage || httpError.statusMessage,
167
+ responseBody: httpError.response?.body || httpError.response?.data,
97
168
  });
98
169
  return {
99
170
  success: false,
@@ -101,6 +172,11 @@ class ComfyUIClient {
101
172
  };
102
173
  }
103
174
  }
175
+ /**
176
+ * Prepare workflow prompt for ComfyUI API
177
+ * @param workflow - Workflow object containing nodes
178
+ * @returns Formatted prompt object
179
+ */
104
180
  preparePrompt(workflow) {
105
181
  const prompt = {};
106
182
  for (const nodeId in workflow) {
@@ -112,29 +188,39 @@ class ComfyUIClient {
112
188
  }
113
189
  return prompt;
114
190
  }
191
+ /**
192
+ * Wait for workflow execution to complete
193
+ * @param promptId - Prompt ID from ComfyUI
194
+ * @param maxWaitTime - Maximum time to wait in milliseconds
195
+ * @returns Promise containing workflow execution result
196
+ */
115
197
  async waitForExecution(promptId, maxWaitTime = constants_1.VALIDATION.MAX_WAIT_TIME_MS) {
116
198
  const startTime = Date.now();
117
199
  let lastStatus = 'pending';
118
200
  let consecutiveErrors = 0;
119
- const maxConsecutiveErrors = 5;
201
+ const maxConsecutiveErrors = constants_1.VALIDATION.MAX_CONSECUTIVE_ERRORS;
202
+ let totalErrors = 0;
203
+ const maxTotalErrors = constants_1.VALIDATION.MAX_TOTAL_ERRORS;
204
+ let backoffDelay = constants_1.VALIDATION.POLL_INTERVAL_MS;
205
+ const maxBackoffDelay = constants_1.VALIDATION.MAX_BACKOFF_DELAY_POLLING;
206
+ const baseDelay = constants_1.VALIDATION.POLL_INTERVAL_MS;
120
207
  while (Date.now() - startTime < maxWaitTime) {
121
208
  try {
122
- if (this.isDestroyed) {
209
+ if (this.isClientDestroyed()) {
123
210
  return {
124
211
  success: false,
125
212
  error: 'Client has been destroyed',
126
213
  };
127
214
  }
128
- const response = await this.helpers.httpRequest({
129
- method: 'GET',
130
- url: `${this.baseUrl}/history/${promptId}`,
215
+ const response = await this.httpClient.get(`${this.baseUrl}/history/${promptId}`, {
131
216
  json: true,
132
- timeout: this.timeout,
217
+ abortSignal: this.currentAbortController?.signal,
133
218
  });
134
219
  if (response[promptId]) {
135
- const status = response[promptId].status;
220
+ const promptData = response[promptId];
221
+ const status = promptData.status;
136
222
  if (status.completed) {
137
- return this.extractResults(response[promptId].outputs);
223
+ return this.extractResults(promptData.outputs);
138
224
  }
139
225
  if (lastStatus !== status.status_str) {
140
226
  lastStatus = status.status_str;
@@ -142,19 +228,33 @@ class ComfyUIClient {
142
228
  }
143
229
  // Reset error counter on successful request
144
230
  consecutiveErrors = 0;
145
- // Yield control between polls
146
- await Promise.resolve();
231
+ backoffDelay = baseDelay; // Reset backoff delay
232
+ // Wait before next poll
233
+ await this.delay(baseDelay);
147
234
  }
148
235
  catch (error) {
149
236
  consecutiveErrors++;
237
+ totalErrors++;
238
+ const errorMsg = error instanceof Error ? error.message : String(error);
239
+ // Check consecutive errors limit
150
240
  if (consecutiveErrors >= maxConsecutiveErrors) {
151
241
  return {
152
242
  success: false,
153
- error: `Workflow execution failed after ${maxConsecutiveErrors} consecutive errors: ${error.message}`,
243
+ error: `Workflow execution failed after ${maxConsecutiveErrors} consecutive errors: ${errorMsg}`,
244
+ };
245
+ }
246
+ // Check total errors limit to prevent resource exhaustion
247
+ if (totalErrors >= maxTotalErrors) {
248
+ return {
249
+ success: false,
250
+ error: `Workflow execution failed after ${maxTotalErrors} total errors (last: ${errorMsg})`,
154
251
  };
155
252
  }
156
- // Yield control between retries
157
- await Promise.resolve();
253
+ // Exponential backoff: double the delay with each error, capped at maxBackoffDelay
254
+ backoffDelay = Math.min(backoffDelay * 2, maxBackoffDelay);
255
+ this.logger.warn(`Polling error ${consecutiveErrors}/${maxConsecutiveErrors} (total: ${totalErrors}/${maxTotalErrors}), retrying in ${backoffDelay}ms: ${errorMsg}`);
256
+ // Wait with exponential backoff
257
+ await this.delay(backoffDelay);
158
258
  }
159
259
  }
160
260
  return {
@@ -162,6 +262,16 @@ class ComfyUIClient {
162
262
  error: 'Workflow execution timeout',
163
263
  };
164
264
  }
265
+ /**
266
+ * Extract image and video results from workflow outputs
267
+ *
268
+ * Note: The `outputs` parameter uses `WorkflowOutputs` type because ComfyUI API responses
269
+ * have a dynamic structure that is not guaranteed. Different ComfyUI nodes
270
+ * may return different output formats, and we need to handle this flexibility.
271
+ *
272
+ * @param outputs - Raw output data from ComfyUI (format is not guaranteed, using WorkflowOutputs for flexibility)
273
+ * @returns WorkflowResult with extracted images and videos
274
+ */
165
275
  extractResults(outputs) {
166
276
  const result = {
167
277
  success: true,
@@ -169,122 +279,288 @@ class ComfyUIClient {
169
279
  videos: [],
170
280
  output: outputs,
171
281
  };
172
- for (const nodeId in outputs) {
173
- const nodeOutput = outputs[nodeId];
282
+ const outputsRecord = outputs;
283
+ for (const nodeId in outputsRecord) {
284
+ const nodeOutput = outputsRecord[nodeId];
285
+ if (!nodeOutput || typeof nodeOutput !== 'object') {
286
+ continue;
287
+ }
288
+ const nodeOutputObj = nodeOutput;
174
289
  // Process images
175
- if (nodeOutput.images && Array.isArray(nodeOutput.images)) {
176
- for (const image of nodeOutput.images) {
177
- const imageUrl = `/view?filename=${image.filename}&subfolder=${image.subfolder || ''}&type=${image.type}`;
178
- result.images.push(imageUrl);
290
+ if (nodeOutputObj.images && Array.isArray(nodeOutputObj.images)) {
291
+ for (const image of nodeOutputObj.images) {
292
+ const imageObj = image;
293
+ const imageUrl = `/view?filename=${imageObj.filename}&subfolder=${imageObj.subfolder || ''}&type=${imageObj.type}`;
294
+ result.images?.push(imageUrl);
179
295
  }
180
296
  }
181
297
  // Process videos (videos array)
182
- if (nodeOutput.videos && Array.isArray(nodeOutput.videos)) {
183
- for (const video of nodeOutput.videos) {
184
- const videoUrl = `/view?filename=${video.filename}&subfolder=${video.subfolder || ''}&type=${video.type}`;
185
- result.videos.push(videoUrl);
298
+ if (nodeOutputObj.videos && Array.isArray(nodeOutputObj.videos)) {
299
+ for (const video of nodeOutputObj.videos) {
300
+ const videoObj = video;
301
+ const videoUrl = `/view?filename=${videoObj.filename}&subfolder=${videoObj.subfolder || ''}&type=${videoObj.type}`;
302
+ result.videos?.push(videoUrl);
186
303
  }
187
304
  }
188
305
  // Process videos (gifs array) - some nodes use this name
189
- if (nodeOutput.gifs && Array.isArray(nodeOutput.gifs)) {
190
- for (const video of nodeOutput.gifs) {
191
- const videoUrl = `/view?filename=${video.filename}&subfolder=${video.subfolder || ''}&type=${video.type}`;
192
- result.videos.push(videoUrl);
306
+ if (nodeOutputObj.gifs && Array.isArray(nodeOutputObj.gifs)) {
307
+ for (const video of nodeOutputObj.gifs) {
308
+ const videoObj = video;
309
+ const videoUrl = `/view?filename=${videoObj.filename}&subfolder=${videoObj.subfolder || ''}&type=${videoObj.type}`;
310
+ result.videos?.push(videoUrl);
193
311
  }
194
312
  }
195
313
  }
196
314
  return result;
197
315
  }
316
+ /**
317
+ * Get execution history from ComfyUI
318
+ * @param limit - Maximum number of history entries to retrieve
319
+ * @returns Promise containing history data
320
+ */
198
321
  async getHistory(limit = 100) {
199
- if (this.isDestroyed) {
322
+ if (this.isClientDestroyed()) {
200
323
  throw new Error('Client has been destroyed');
201
324
  }
202
- const response = await this.retryRequest(() => this.helpers.httpRequest({
203
- method: 'GET',
204
- url: `${this.baseUrl}/history`,
325
+ const response = await this.retryRequest(() => this.httpClient.get(`${this.baseUrl}/history`, {
205
326
  qs: { limit },
206
327
  json: true,
207
- timeout: this.timeout,
328
+ abortSignal: this.currentAbortController?.signal,
208
329
  }));
209
330
  return response;
210
331
  }
332
+ /**
333
+ * Upload an image to ComfyUI server
334
+ * @param imageData - Image data as Buffer
335
+ * @param filename - Name of the file to upload
336
+ * @param overwrite - Whether to overwrite existing file
337
+ * @returns Promise containing the uploaded filename
338
+ * @throws Error if image data is invalid or upload fails
339
+ */
211
340
  async uploadImage(imageData, filename, overwrite = false) {
212
- if (this.isDestroyed) {
341
+ if (this.isClientDestroyed()) {
213
342
  throw new Error('Client has been destroyed');
214
343
  }
215
- console.log('[ComfyUI] Uploading image:', { filename, size: imageData.length });
344
+ // Validate image data
345
+ if (!Buffer.isBuffer(imageData)) {
346
+ throw new Error('Invalid image data: expected Buffer');
347
+ }
348
+ if (imageData.length === 0) {
349
+ throw new Error('Invalid image data: buffer is empty');
350
+ }
351
+ // Validate image size (maximum 50MB as defined in VALIDATION.MAX_IMAGE_SIZE_MB)
352
+ const maxSize = (0, utils_1.getMaxImageSizeBytes)();
353
+ if (imageData.length > maxSize) {
354
+ throw new Error(`Image size (${(0, utils_1.formatBytes)(imageData.length)}) exceeds maximum allowed size of ${(0, utils_1.formatBytes)(maxSize)}`);
355
+ }
356
+ this.logger.debug('Uploading image:', { filename, size: imageData.length });
216
357
  const form = new form_data_1.default();
217
358
  form.append('image', imageData, { filename: filename });
218
359
  form.append('overwrite', overwrite.toString());
219
- const response = await this.retryRequest(() => this.helpers.httpRequest({
360
+ const response = await this.retryRequest(() => this.httpClient.request({
220
361
  method: 'POST',
221
362
  url: `${this.baseUrl}/upload/image`,
222
363
  body: form,
223
364
  headers: {
224
365
  ...form.getHeaders(),
225
366
  },
226
- timeout: this.timeout,
367
+ abortSignal: this.currentAbortController?.signal,
227
368
  }));
228
- console.log('[ComfyUI] Upload response:', response);
369
+ this.logger.debug('Upload response:', response);
229
370
  return response.name;
230
371
  }
372
+ /**
373
+ * Get system information from ComfyUI server
374
+ * @returns Promise containing system stats
375
+ */
231
376
  async getSystemInfo() {
232
- if (this.isDestroyed) {
377
+ if (this.isClientDestroyed()) {
233
378
  throw new Error('Client has been destroyed');
234
379
  }
235
- const response = await this.retryRequest(() => this.helpers.httpRequest({
236
- method: 'GET',
237
- url: `${this.baseUrl}/system_stats`,
380
+ const response = await this.retryRequest(() => this.httpClient.get(`${this.baseUrl}/system_stats`, {
238
381
  json: true,
239
- timeout: this.timeout,
382
+ abortSignal: this.currentAbortController?.signal,
240
383
  }));
241
384
  return response;
242
385
  }
243
- async getImageBuffer(imagePath) {
386
+ /**
387
+ * Check if ComfyUI server is accessible
388
+ * @returns Promise containing health status
389
+ */
390
+ async healthCheck() {
244
391
  try {
245
- if (this.isDestroyed) {
246
- throw new Error('Client has been destroyed');
247
- }
248
- const response = await this.helpers.httpRequest({
249
- method: 'GET',
250
- url: `${this.baseUrl}${imagePath}`,
251
- encoding: 'arraybuffer',
252
- timeout: this.timeout,
392
+ await this.httpClient.get(`${this.baseUrl}/system_stats`, {
393
+ json: true,
394
+ timeout: 5000,
253
395
  });
254
- return Buffer.from(response);
396
+ return {
397
+ healthy: true,
398
+ message: 'ComfyUI server is accessible',
399
+ };
255
400
  }
256
401
  catch (error) {
257
- throw new Error(`Failed to get image buffer: ${this.formatErrorMessage(error)}`);
402
+ return {
403
+ healthy: false,
404
+ message: `ComfyUI server is not accessible: ${error instanceof Error ? error.message : String(error)}`,
405
+ };
258
406
  }
259
407
  }
260
- async getVideoBuffer(videoPath) {
408
+ /**
409
+ * Get buffer from ComfyUI server (internal method)
410
+ * @param path - Path to the resource on ComfyUI server
411
+ * @param resourceType - Type of resource ('image' or 'video')
412
+ * @returns Promise containing resource data as Buffer
413
+ * @throws Error if resource retrieval fails
414
+ */
415
+ async getBuffer(path, resourceType) {
261
416
  try {
262
- if (this.isDestroyed) {
417
+ if (this.isClientDestroyed()) {
263
418
  throw new Error('Client has been destroyed');
264
419
  }
265
- const response = await this.helpers.httpRequest({
266
- method: 'GET',
267
- url: `${this.baseUrl}${videoPath}`,
420
+ const response = await this.httpClient.get(`${this.baseUrl}${path}`, {
268
421
  encoding: 'arraybuffer',
269
- timeout: this.timeout,
422
+ abortSignal: this.currentAbortController?.signal,
270
423
  });
271
- return Buffer.from(response);
424
+ const buffer = Buffer.from(response);
425
+ // Validate buffer size (maximum 50MB as defined in VALIDATION.MAX_IMAGE_SIZE_MB)
426
+ const maxSize = (0, utils_1.getMaxImageSizeBytes)();
427
+ if (buffer.length > maxSize) {
428
+ const resourceTypeLabel = resourceType.charAt(0).toUpperCase() + resourceType.slice(1);
429
+ throw new Error(`${resourceTypeLabel} size (${(0, utils_1.formatBytes)(buffer.length)}) exceeds maximum allowed size of ${(0, utils_1.formatBytes)(maxSize)}`);
430
+ }
431
+ return buffer;
272
432
  }
273
433
  catch (error) {
274
- throw new Error(`Failed to get video buffer: ${this.formatErrorMessage(error)}`);
434
+ throw new Error(`Failed to get ${resourceType} buffer: ${this.formatErrorMessage(error)}`);
275
435
  }
276
436
  }
437
+ /**
438
+ * Get image buffer from ComfyUI server
439
+ * @param imagePath - Path to the image on ComfyUI server
440
+ * @returns Promise containing image data as Buffer
441
+ * @throws Error if image retrieval fails
442
+ */
443
+ async getImageBuffer(imagePath) {
444
+ return this.getBuffer(imagePath, 'image');
445
+ }
446
+ /**
447
+ * Get video buffer from ComfyUI server
448
+ * @param videoPath - Path to the video on ComfyUI server
449
+ * @returns Promise containing video data as Buffer
450
+ * @throws Error if video retrieval fails
451
+ */
452
+ async getVideoBuffer(videoPath) {
453
+ return this.getBuffer(videoPath, 'video');
454
+ }
455
+ /**
456
+ * Get multiple image buffers concurrently from ComfyUI server
457
+ * @param imagePaths - Array of paths to the images on ComfyUI server
458
+ * @returns Promise containing array of image data as Buffers
459
+ * @throws Error if any image retrieval fails
460
+ */
461
+ async getImageBuffers(imagePaths) {
462
+ return Promise.all(imagePaths.map(path => this.getImageBuffer(path)));
463
+ }
464
+ /**
465
+ * Get multiple video buffers concurrently from ComfyUI server
466
+ * @param videoPaths - Array of paths to the videos on ComfyUI server
467
+ * @returns Promise containing array of video data as Buffers
468
+ * @throws Error if any video retrieval fails
469
+ */
470
+ async getVideoBuffers(videoPaths) {
471
+ return Promise.all(videoPaths.map(path => this.getVideoBuffer(path)));
472
+ }
277
473
  /**
278
474
  * Format error message with additional context
279
475
  */
280
476
  formatErrorMessage(error, context = '') {
281
- if (error.response) {
282
- return `${context}: ${error.response.statusCode} ${error.response.statusMessage}`;
477
+ if (error instanceof Error) {
478
+ const httpError = error;
479
+ if (httpError.response) {
480
+ return `${context}: ${httpError.response.statusCode} ${httpError.response.statusMessage}`;
481
+ }
482
+ else if (httpError.statusCode) {
483
+ return `${context}: ${httpError.statusCode} ${httpError.statusMessage || ''}`;
484
+ }
485
+ return context ? `${context}: ${error.message}` : error.message;
486
+ }
487
+ return context ? `${context}: ${String(error)}` : String(error);
488
+ }
489
+ /**
490
+ * Process workflow execution results and format them for n8n output
491
+ * @param result - Workflow execution result
492
+ * @param outputBinaryKey - Property name for the first output binary data
493
+ * @returns Processed result with json and binary data
494
+ */
495
+ async processResults(result, outputBinaryKey = 'data') {
496
+ const jsonData = {
497
+ success: true,
498
+ };
499
+ if (result.output) {
500
+ jsonData.data = result.output;
501
+ }
502
+ if (result.images && result.images.length > 0) {
503
+ jsonData.images = result.images;
504
+ jsonData.imageUrls = result.images.map(img => `${this.baseUrl}${img}`);
283
505
  }
284
- else if (error.request) {
285
- return `${context}: No response from server`;
506
+ if (result.videos && result.videos.length > 0) {
507
+ jsonData.videos = result.videos;
508
+ jsonData.videoUrls = result.videos.map(vid => `${this.baseUrl}${vid}`);
509
+ }
510
+ const binaryData = {};
511
+ const buffers = [];
512
+ try {
513
+ // Fetch and process images concurrently
514
+ if (result.images && result.images.length > 0) {
515
+ const imageBuffers = await this.getImageBuffers(result.images);
516
+ buffers.push(...imageBuffers);
517
+ for (let i = 0; i < result.images.length; i++) {
518
+ const imagePath = result.images[i];
519
+ const imageBuffer = imageBuffers[i];
520
+ const fileInfo = (0, utils_1.extractFileInfo)(imagePath, 'png');
521
+ const mimeType = (0, utils_1.validateMimeType)(fileInfo.mimeType, constants_1.IMAGE_MIME_TYPES);
522
+ const binaryKey = i === 0 ? outputBinaryKey : `image_${i}`;
523
+ binaryData[binaryKey] = {
524
+ data: imageBuffer.toString('base64'),
525
+ mimeType: mimeType,
526
+ fileName: fileInfo.filename,
527
+ };
528
+ }
529
+ jsonData.imageCount = result.images.length;
530
+ }
531
+ // Fetch and process videos concurrently
532
+ if (result.videos && result.videos.length > 0) {
533
+ const videoBuffers = await this.getVideoBuffers(result.videos);
534
+ buffers.push(...videoBuffers);
535
+ for (let i = 0; i < result.videos.length; i++) {
536
+ const videoPath = result.videos[i];
537
+ const videoBuffer = videoBuffers[i];
538
+ const fileInfo = (0, utils_1.extractFileInfo)(videoPath, 'mp4');
539
+ const mimeType = (0, utils_1.validateMimeType)(fileInfo.mimeType, constants_1.VIDEO_MIME_TYPES);
540
+ const hasImages = result.images && result.images.length > 0;
541
+ const binaryKey = (!hasImages && i === 0) ? outputBinaryKey : `video_${i}`;
542
+ binaryData[binaryKey] = {
543
+ data: videoBuffer.toString('base64'),
544
+ mimeType: mimeType,
545
+ fileName: fileInfo.filename,
546
+ };
547
+ }
548
+ jsonData.videoCount = result.videos.length;
549
+ }
550
+ return {
551
+ json: jsonData,
552
+ binary: binaryData,
553
+ };
554
+ }
555
+ catch (error) {
556
+ // Clean up already fetched buffers to prevent memory leaks
557
+ buffers.forEach(buffer => {
558
+ if (buffer) {
559
+ buffer.fill(0); // Zero out sensitive data
560
+ }
561
+ });
562
+ throw error;
286
563
  }
287
- return context ? `${context}: ${error.message}` : error.message;
288
564
  }
289
565
  }
290
566
  exports.ComfyUIClient = ComfyUIClient;