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.
- package/LICENSE +1 -1
- package/README.md +126 -11
- package/dist/agent-tools/ComfyUIAgentTool.d.ts +62 -0
- package/dist/agent-tools/ComfyUIAgentTool.d.ts.map +1 -0
- package/dist/agent-tools/ComfyUIAgentTool.js +440 -0
- package/dist/agent-tools/ComfyUIAgentTool.js.map +1 -0
- package/dist/nodes/AxiosAdapter.d.ts +5 -0
- package/dist/nodes/AxiosAdapter.d.ts.map +1 -0
- package/dist/nodes/AxiosAdapter.js +31 -0
- package/dist/nodes/AxiosAdapter.js.map +1 -0
- package/dist/nodes/ComfyUi/ComfyUi.node.d.ts +30 -0
- package/dist/nodes/ComfyUi/ComfyUi.node.d.ts.map +1 -1
- package/dist/nodes/ComfyUi/ComfyUi.node.js +47 -251
- package/dist/nodes/ComfyUi/ComfyUi.node.js.map +1 -1
- package/dist/nodes/ComfyUiClient.d.ts +116 -3
- package/dist/nodes/ComfyUiClient.d.ts.map +1 -1
- package/dist/nodes/ComfyUiClient.js +374 -98
- package/dist/nodes/ComfyUiClient.js.map +1 -1
- package/dist/nodes/HttpClient.d.ts +34 -0
- package/dist/nodes/HttpClient.d.ts.map +1 -0
- package/dist/nodes/HttpClient.js +85 -0
- package/dist/nodes/HttpClient.js.map +1 -0
- package/dist/nodes/N8nHelpersAdapter.d.ts +8 -0
- package/dist/nodes/N8nHelpersAdapter.d.ts.map +1 -0
- package/dist/nodes/N8nHelpersAdapter.js +23 -0
- package/dist/nodes/N8nHelpersAdapter.js.map +1 -0
- package/dist/nodes/constants.d.ts +14 -3
- package/dist/nodes/constants.d.ts.map +1 -1
- package/dist/nodes/constants.js +20 -14
- package/dist/nodes/constants.js.map +1 -1
- package/dist/nodes/errors.d.ts +35 -0
- package/dist/nodes/errors.d.ts.map +1 -0
- package/dist/nodes/errors.js +111 -0
- package/dist/nodes/errors.js.map +1 -0
- package/dist/nodes/logger.d.ts +9 -17
- package/dist/nodes/logger.d.ts.map +1 -1
- package/dist/nodes/logger.js +72 -23
- package/dist/nodes/logger.js.map +1 -1
- package/dist/nodes/parameterProcessor.d.ts +22 -0
- package/dist/nodes/parameterProcessor.d.ts.map +1 -0
- package/dist/nodes/parameterProcessor.js +268 -0
- package/dist/nodes/parameterProcessor.js.map +1 -0
- package/dist/nodes/types.d.ts +129 -0
- package/dist/nodes/types.d.ts.map +1 -1
- package/dist/nodes/utils.d.ts +65 -0
- package/dist/nodes/utils.d.ts.map +1 -0
- package/dist/nodes/utils.js +172 -0
- package/dist/nodes/utils.js.map +1 -0
- package/dist/nodes/validation.d.ts +8 -0
- package/dist/nodes/validation.d.ts.map +1 -1
- package/dist/nodes/validation.js +129 -7
- package/dist/nodes/validation.js.map +1 -1
- package/dist/nodes/workflowConfig.d.ts +13 -0
- package/dist/nodes/workflowConfig.d.ts.map +1 -0
- package/dist/nodes/workflowConfig.js +91 -0
- package/dist/nodes/workflowConfig.js.map +1 -0
- 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.
|
|
13
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
timeout: this.timeout,
|
|
148
|
+
abortSignal: this.currentAbortController?.signal,
|
|
80
149
|
}));
|
|
81
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
217
|
+
abortSignal: this.currentAbortController?.signal,
|
|
133
218
|
});
|
|
134
219
|
if (response[promptId]) {
|
|
135
|
-
const
|
|
220
|
+
const promptData = response[promptId];
|
|
221
|
+
const status = promptData.status;
|
|
136
222
|
if (status.completed) {
|
|
137
|
-
return this.extractResults(
|
|
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
|
-
//
|
|
146
|
-
|
|
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: ${
|
|
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
|
-
//
|
|
157
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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 (
|
|
176
|
-
for (const image of
|
|
177
|
-
const
|
|
178
|
-
|
|
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 (
|
|
183
|
-
for (const video of
|
|
184
|
-
const
|
|
185
|
-
|
|
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 (
|
|
190
|
-
for (const video of
|
|
191
|
-
const
|
|
192
|
-
|
|
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.
|
|
322
|
+
if (this.isClientDestroyed()) {
|
|
200
323
|
throw new Error('Client has been destroyed');
|
|
201
324
|
}
|
|
202
|
-
const response = await this.retryRequest(() => this.
|
|
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
|
-
|
|
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.
|
|
341
|
+
if (this.isClientDestroyed()) {
|
|
213
342
|
throw new Error('Client has been destroyed');
|
|
214
343
|
}
|
|
215
|
-
|
|
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.
|
|
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
|
-
|
|
367
|
+
abortSignal: this.currentAbortController?.signal,
|
|
227
368
|
}));
|
|
228
|
-
|
|
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.
|
|
377
|
+
if (this.isClientDestroyed()) {
|
|
233
378
|
throw new Error('Client has been destroyed');
|
|
234
379
|
}
|
|
235
|
-
const response = await this.retryRequest(() => this.
|
|
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
|
-
|
|
382
|
+
abortSignal: this.currentAbortController?.signal,
|
|
240
383
|
}));
|
|
241
384
|
return response;
|
|
242
385
|
}
|
|
243
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Check if ComfyUI server is accessible
|
|
388
|
+
* @returns Promise containing health status
|
|
389
|
+
*/
|
|
390
|
+
async healthCheck() {
|
|
244
391
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
396
|
+
return {
|
|
397
|
+
healthy: true,
|
|
398
|
+
message: 'ComfyUI server is accessible',
|
|
399
|
+
};
|
|
255
400
|
}
|
|
256
401
|
catch (error) {
|
|
257
|
-
|
|
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
|
-
|
|
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.
|
|
417
|
+
if (this.isClientDestroyed()) {
|
|
263
418
|
throw new Error('Client has been destroyed');
|
|
264
419
|
}
|
|
265
|
-
const response = await this.
|
|
266
|
-
method: 'GET',
|
|
267
|
-
url: `${this.baseUrl}${videoPath}`,
|
|
420
|
+
const response = await this.httpClient.get(`${this.baseUrl}${path}`, {
|
|
268
421
|
encoding: 'arraybuffer',
|
|
269
|
-
|
|
422
|
+
abortSignal: this.currentAbortController?.signal,
|
|
270
423
|
});
|
|
271
|
-
|
|
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
|
|
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
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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;
|