n8n-nodes-commandos-image 0.1.8 → 0.1.10

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.
@@ -15,9 +15,6 @@ const resolveGenerationUrl = (resolvedModel) => {
15
15
  }
16
16
  return GENERATION_URL_DEFAULT;
17
17
  };
18
- /**
19
- * Maps standard ratios to specific model requirements
20
- */
21
18
  const mapRatio = (model, ratio) => {
22
19
  if (model.includes('seedream')) {
23
20
  if (ratio === '2:3')
@@ -27,11 +24,10 @@ const mapRatio = (model, ratio) => {
27
24
  }
28
25
  return ratio;
29
26
  };
30
- const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'basic', resolution = '1K', }) => {
27
+ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'basic', resolution = '1K', outputFormat, }) => {
31
28
  const hasReferences = references.length > 0;
32
29
  const requestedModel = model;
33
30
  let resolvedModel = model;
34
- // Model Resolution
35
31
  if (requestedModel === 'flux-pro') {
36
32
  resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
37
33
  }
@@ -44,12 +40,8 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
44
40
  else if (requestedModel === 'seedream') {
45
41
  resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
46
42
  }
47
- // Fallback for MJ or generic seedream
48
43
  if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
49
- if (requestedModel === 'seedream') {
50
- // Handled above for 4.5
51
- }
52
- else if (requestedModel === 'midjourney') {
44
+ if (requestedModel === 'midjourney') {
53
45
  resolvedModel = 'midjourney';
54
46
  }
55
47
  }
@@ -59,7 +51,7 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
59
51
  filesUrl: references,
60
52
  prompt,
61
53
  size: ratio,
62
- callBackUrl: 'https://api.comandos.ai/internal/kie/callback', // Fixed to internal
54
+ callBackUrl: 'https://api.comandos.ai/internal/kie/callback',
63
55
  fallbackModel: 'FLUX_MAX',
64
56
  };
65
57
  }
@@ -70,6 +62,7 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
70
62
  prompt,
71
63
  aspect_ratio: ratio,
72
64
  resolution,
65
+ output_format: 'jpeg',
73
66
  ...(hasReferences ? { input_urls: references } : {}),
74
67
  },
75
68
  };
@@ -81,6 +74,7 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
81
74
  prompt,
82
75
  aspect_ratio: mapRatio(resolvedModel, ratio),
83
76
  quality,
77
+ output_format: 'jpeg',
84
78
  ...(hasReferences ? { image_urls: references } : {}),
85
79
  },
86
80
  };
@@ -92,7 +86,7 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
92
86
  prompt,
93
87
  aspect_ratio: ratio,
94
88
  resolution,
95
- output_format: 'jpg', // ALWAYS JPG for Nano Banana Pro (lightweight)
89
+ output_format: outputFormat || 'jpg', // User selection or fallback to jpg
96
90
  ...(hasReferences ? { image_input: references } : {}),
97
91
  },
98
92
  };
@@ -103,7 +97,7 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
103
97
  input: {
104
98
  prompt,
105
99
  image_size: ratio,
106
- output_format: 'jpeg', // ALWAYS JPEG for Nano Banana (lightweight)
100
+ output_format: 'jpeg',
107
101
  ...(hasReferences ? { image_urls: references } : {}),
108
102
  },
109
103
  };
@@ -119,13 +113,13 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
119
113
  };
120
114
  }
121
115
  else {
122
- // Default fallback (v4 edit style)
123
116
  body = {
124
117
  model: requestedModel === 'gpt4o-image' ? 'gpt-4o' : 'bytedance/seedream-v4-edit',
125
118
  input: {
126
119
  image_urls: references,
127
120
  prompt,
128
121
  image_size: mapRatio('seedream', ratio),
122
+ output_format: 'jpeg',
129
123
  },
130
124
  };
131
125
  }
@@ -140,12 +134,10 @@ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'b
140
134
  };
141
135
  };
142
136
  const extractReferences = (raw, model) => {
143
- if (!raw || typeof raw !== 'object') {
137
+ if (!raw || typeof raw !== 'object')
144
138
  return [];
145
- }
146
139
  const collection = raw;
147
140
  const items = Array.isArray(collection.reference) ? collection.reference : [];
148
- // Dynamic limit based on model docs
149
141
  let limit = 8;
150
142
  if (model === 'seedream')
151
143
  limit = 14;
@@ -153,10 +145,7 @@ const extractReferences = (raw, model) => {
153
145
  limit = 16;
154
146
  if (model.includes('nano-banana'))
155
147
  limit = 10;
156
- return items
157
- .map((entry) => String((entry === null || entry === void 0 ? void 0 : entry.url) || '').trim())
158
- .filter((value) => value.length > 0 && /^https?:\/\//i.test(value))
159
- .slice(0, limit);
148
+ return items.map((entry) => String((entry === null || entry === void 0 ? void 0 : entry.url) || '').trim()).filter((v) => v.length > 0 && /^https?:\/\//i.test(v)).slice(0, limit);
160
149
  };
161
150
  class CommandosImage {
162
151
  constructor() {
@@ -166,18 +155,11 @@ class CommandosImage {
166
155
  group: ['transform'],
167
156
  version: 1,
168
157
  description: 'Create image tasks in Commandos API',
169
- defaults: {
170
- name: 'Commandos Image',
171
- },
158
+ defaults: { name: 'Commandos Image' },
172
159
  icon: 'file:Image.png',
173
160
  inputs: ['main'],
174
161
  outputs: ['main'],
175
- credentials: [
176
- {
177
- name: 'commandosApi',
178
- required: true,
179
- },
180
- ],
162
+ credentials: [{ name: 'commandosApi', required: true }],
181
163
  properties: [
182
164
  {
183
165
  displayName: 'Operation',
@@ -185,14 +167,8 @@ class CommandosImage {
185
167
  type: 'options',
186
168
  noDataExpression: true,
187
169
  options: [
188
- {
189
- name: 'Create Task',
190
- value: 'create',
191
- },
192
- {
193
- name: 'Check Status',
194
- value: 'status',
195
- },
170
+ { name: 'Create Task', value: 'create' },
171
+ { name: 'Check Status', value: 'status' },
196
172
  ],
197
173
  default: 'create',
198
174
  },
@@ -209,25 +185,15 @@ class CommandosImage {
209
185
  { name: 'Midjourney', value: 'midjourney' },
210
186
  ],
211
187
  default: 'seedream',
212
- displayOptions: {
213
- show: {
214
- operation: ['create'],
215
- },
216
- },
188
+ displayOptions: { show: { operation: ['create'] } },
217
189
  },
218
190
  {
219
191
  displayName: 'Prompt',
220
192
  name: 'prompt',
221
193
  type: 'string',
222
194
  default: '',
223
- typeOptions: {
224
- rows: 4,
225
- },
226
- displayOptions: {
227
- show: {
228
- operation: ['create'],
229
- },
230
- },
195
+ typeOptions: { rows: 4 },
196
+ displayOptions: { show: { operation: ['create'] } },
231
197
  },
232
198
  {
233
199
  displayName: 'Ratio',
@@ -246,9 +212,35 @@ class CommandosImage {
246
212
  ],
247
213
  default: '2:3',
248
214
  displayOptions: {
249
- show: {
250
- operation: ['create'],
251
- },
215
+ show: { operation: ['create'] },
216
+ hide: { model: ['gpt4o-image'] },
217
+ },
218
+ },
219
+ {
220
+ displayName: 'Ratio',
221
+ name: 'ratioGpt',
222
+ type: 'options',
223
+ options: [
224
+ { name: '1:1', value: '1:1' },
225
+ { name: '2:3', value: '2:3' },
226
+ { name: '3:2', value: '3:2' },
227
+ ],
228
+ default: '2:3',
229
+ displayOptions: {
230
+ show: { operation: ['create'], model: ['gpt4o-image'] },
231
+ },
232
+ },
233
+ {
234
+ displayName: 'Output Format',
235
+ name: 'outputFormatBananaPro',
236
+ type: 'options',
237
+ options: [
238
+ { name: 'PNG', value: 'png' },
239
+ { name: 'JPG', value: 'jpg' },
240
+ ],
241
+ default: 'jpg',
242
+ displayOptions: {
243
+ show: { operation: ['create'], model: ['nano-banana-pro'] },
252
244
  },
253
245
  },
254
246
  {
@@ -262,10 +254,7 @@ class CommandosImage {
262
254
  ],
263
255
  default: 'basic',
264
256
  displayOptions: {
265
- show: {
266
- operation: ['create'],
267
- model: ['seedream', 'gpt4o-image', 'flux-pro'],
268
- },
257
+ show: { operation: ['create'], model: ['seedream', 'gpt4o-image', 'flux-pro'] },
269
258
  },
270
259
  },
271
260
  {
@@ -279,10 +268,7 @@ class CommandosImage {
279
268
  ],
280
269
  default: '1K',
281
270
  displayOptions: {
282
- show: {
283
- operation: ['create'],
284
- model: ['flux-pro', 'nano-banana-pro'],
285
- },
271
+ show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
286
272
  },
287
273
  },
288
274
  {
@@ -290,39 +276,22 @@ class CommandosImage {
290
276
  name: 'references',
291
277
  type: 'fixedCollection',
292
278
  default: {},
293
- typeOptions: {
294
- multipleValues: true,
295
- },
279
+ typeOptions: { multipleValues: true },
296
280
  options: [
297
281
  {
298
282
  name: 'reference',
299
283
  displayName: 'Reference',
300
- values: [
301
- {
302
- displayName: 'Reference URL',
303
- name: 'url',
304
- type: 'string',
305
- default: '',
306
- },
307
- ],
284
+ values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
308
285
  },
309
286
  ],
310
- displayOptions: {
311
- show: {
312
- operation: ['create'],
313
- },
314
- },
287
+ displayOptions: { show: { operation: ['create'] } },
315
288
  },
316
289
  {
317
290
  displayName: 'Task ID',
318
291
  name: 'taskId',
319
292
  type: 'string',
320
293
  default: '',
321
- displayOptions: {
322
- show: {
323
- operation: ['status'],
324
- },
325
- },
294
+ displayOptions: { show: { operation: ['status'] } },
326
295
  },
327
296
  ],
328
297
  };
@@ -332,82 +301,54 @@ class CommandosImage {
332
301
  const operation = this.getNodeParameter('operation', 0);
333
302
  const credentials = await this.getCredentials('commandosApi');
334
303
  const licenseKey = String(credentials.licenseKey || '').trim();
335
- if (!licenseKey) {
304
+ if (!licenseKey)
336
305
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'License key is required');
337
- }
338
306
  const results = [];
339
307
  for (let i = 0; i < items.length; i += 1) {
340
308
  try {
341
309
  if (operation === 'create') {
342
310
  const model = String(this.getNodeParameter('model', i));
343
311
  const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
344
- const ratio = String(this.getNodeParameter('ratio', i));
345
- const references = extractReferences(this.getNodeParameter('references', i, {}), model);
346
- const quality = ['seedream', 'gpt4o-image', 'flux-pro'].includes(model)
347
- ? String(this.getNodeParameter('quality', i, 'basic'))
348
- : 'basic';
349
- const resolution = ['flux-pro', 'nano-banana-pro'].includes(model)
350
- ? String(this.getNodeParameter('resolution', i, '1K'))
351
- : '1K';
352
- if (!prompt) {
353
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
312
+ let ratio = '';
313
+ if (model === 'gpt4o-image') {
314
+ ratio = String(this.getNodeParameter('ratioGpt', i));
354
315
  }
355
- const request = buildGenerationRequest({
356
- model,
357
- prompt,
358
- ratio,
359
- references,
360
- quality,
361
- resolution
362
- });
363
- if (!request.url) {
364
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'URL генерации не настроен. Проверь переменные окружения COMMANDOS_IMAGE_URL_DEFAULT/COMMANDOS_IMAGE_URL_GPT4O/COMMANDOS_IMAGE_URL_MJ', { itemIndex: i });
316
+ else {
317
+ ratio = String(this.getNodeParameter('ratio', i));
365
318
  }
366
- const payload = {
367
- url: request.url,
368
- body: request.body,
369
- requestedModel: request.requestedModel,
370
- resolvedModel: request.resolvedModel,
371
- ratio: request.ratio,
372
- references: request.references,
373
- hasReferences: request.hasReferences,
374
- _v: '0.1.8'
375
- };
319
+ const references = extractReferences(this.getNodeParameter('references', i, {}), model);
320
+ const quality = ['seedream', 'gpt4o-image', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
321
+ const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
322
+ const outputFormat = (model === 'nano-banana-pro') ? String(this.getNodeParameter('outputFormatBananaPro', i, 'jpg')) : undefined;
323
+ if (!prompt)
324
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
325
+ const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution, outputFormat });
326
+ const payload = { ...request, licenseKey, _v: '0.1.10' };
376
327
  const response = await this.helpers.request({
377
328
  method: 'POST',
378
329
  url: `${COMMANDOS_API_URL}/tasks`,
379
- headers: {
380
- 'Content-Type': 'application/json',
381
- 'X-License-Key': licenseKey,
382
- },
383
- body: {
384
- process_type: 'IMAGE_GENERATION',
385
- payload,
386
- },
330
+ headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
331
+ body: { process_type: 'IMAGE_GENERATION', payload },
387
332
  json: true,
388
333
  });
389
334
  results.push({ json: response });
390
335
  }
391
336
  else if (operation === 'status') {
392
337
  const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
393
- if (!taskId) {
338
+ if (!taskId)
394
339
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
395
- }
396
340
  const response = await this.helpers.request({
397
341
  method: 'GET',
398
342
  url: `${COMMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
399
- headers: {
400
- 'X-License-Key': licenseKey,
401
- },
343
+ headers: { 'X-License-Key': licenseKey },
402
344
  json: true,
403
345
  });
404
346
  results.push({ json: response });
405
347
  }
406
348
  }
407
349
  catch (error) {
408
- if (error instanceof n8n_workflow_1.NodeOperationError) {
350
+ if (error instanceof n8n_workflow_1.NodeOperationError)
409
351
  throw error;
410
- }
411
352
  const message = error instanceof Error ? error.message : 'Request failed';
412
353
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), message, { itemIndex: i });
413
354
  }
@@ -1,9 +1,9 @@
1
1
  import type {
2
- IDataObject,
3
- IExecuteFunctions,
4
- INodeExecutionData,
5
- INodeType,
6
- INodeTypeDescription,
2
+ IDataObject,
3
+ IExecuteFunctions,
4
+ INodeExecutionData,
5
+ INodeType,
6
+ INodeTypeDescription,
7
7
  } from 'n8n-workflow';
8
8
  import { NodeOperationError } from 'n8n-workflow';
9
9
 
@@ -13,448 +13,370 @@ const GENERATION_URL_GPT4O = String(process.env.COMMANDOS_IMAGE_URL_GPT4O || '')
13
13
  const GENERATION_URL_MJ = String(process.env.COMMANDOS_IMAGE_URL_MJ || '').trim();
14
14
 
15
15
  type BuildParams = {
16
- model: string;
17
- prompt: string;
18
- ratio: string;
19
- references: string[];
20
- quality?: string;
21
- resolution?: string;
16
+ model: string;
17
+ prompt: string;
18
+ ratio: string;
19
+ references: string[];
20
+ quality?: string;
21
+ resolution?: string;
22
+ outputFormat?: string;
22
23
  };
23
24
 
24
25
  type BuildResult = {
25
- url: string;
26
- body: Record<string, unknown>;
27
- requestedModel: string;
28
- resolvedModel: string;
29
- ratio: string;
30
- references: string[];
31
- hasReferences: boolean;
26
+ url: string;
27
+ body: Record<string, unknown>;
28
+ requestedModel: string;
29
+ resolvedModel: string;
30
+ ratio: string;
31
+ references: string[];
32
+ hasReferences: boolean;
32
33
  };
33
34
 
34
35
  const resolveGenerationUrl = (resolvedModel: string): string => {
35
- if (resolvedModel === 'gpt4o-image') {
36
- return GENERATION_URL_GPT4O || GENERATION_URL_DEFAULT;
37
- }
38
- if (resolvedModel === 'midjourney') {
39
- return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
40
- }
41
- return GENERATION_URL_DEFAULT;
36
+ if (resolvedModel === 'gpt4o-image') {
37
+ return GENERATION_URL_GPT4O || GENERATION_URL_DEFAULT;
38
+ }
39
+ if (resolvedModel === 'midjourney') {
40
+ return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
41
+ }
42
+ return GENERATION_URL_DEFAULT;
42
43
  };
43
44
 
44
- /**
45
- * Maps standard ratios to specific model requirements
46
- */
47
45
  const mapRatio = (model: string, ratio: string): string => {
48
- if (model.includes('seedream')) {
49
- if (ratio === '2:3') return 'portrait_3_2';
50
- if (ratio === '3:2') return 'landscape_2_3';
51
- }
52
- return ratio;
46
+ if (model.includes('seedream')) {
47
+ if (ratio === '2:3') return 'portrait_3_2';
48
+ if (ratio === '3:2') return 'landscape_2_3';
49
+ }
50
+ return ratio;
53
51
  };
54
52
 
55
53
  const buildGenerationRequest = ({
56
- model,
57
- prompt,
58
- ratio,
59
- references,
60
- quality = 'basic',
61
- resolution = '1K',
54
+ model,
55
+ prompt,
56
+ ratio,
57
+ references,
58
+ quality = 'basic',
59
+ resolution = '1K',
60
+ outputFormat,
62
61
  }: BuildParams): BuildResult => {
63
- const hasReferences = references.length > 0;
64
- const requestedModel = model;
65
- let resolvedModel = model;
62
+ const hasReferences = references.length > 0;
63
+ const requestedModel = model;
64
+ let resolvedModel = model;
66
65
 
67
- // Model Resolution
68
- if (requestedModel === 'flux-pro') {
69
- resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
70
- } else if (requestedModel === 'nano-banana') {
71
- resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
72
- } else if (requestedModel === 'nano-banana-pro') {
73
- resolvedModel = 'nano-banana-pro';
74
- } else if (requestedModel === 'seedream') {
75
- resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
76
- }
66
+ if (requestedModel === 'flux-pro') {
67
+ resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
68
+ } else if (requestedModel === 'nano-banana') {
69
+ resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
70
+ } else if (requestedModel === 'nano-banana-pro') {
71
+ resolvedModel = 'nano-banana-pro';
72
+ } else if (requestedModel === 'seedream') {
73
+ resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
74
+ }
77
75
 
78
- // Fallback for MJ or generic seedream
79
- if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
80
- if (requestedModel === 'seedream') {
81
- // Handled above for 4.5
82
- } else if (requestedModel === 'midjourney') {
83
- resolvedModel = 'midjourney';
76
+ if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
77
+ if (requestedModel === 'midjourney') {
78
+ resolvedModel = 'midjourney';
79
+ }
84
80
  }
85
- }
86
81
 
87
- let body: Record<string, unknown> = {};
82
+ let body: Record<string, unknown> = {};
88
83
 
89
- if (resolvedModel === 'gpt4o-image') {
90
- body = {
91
- filesUrl: references,
92
- prompt,
93
- size: ratio,
94
- callBackUrl: 'https://api.comandos.ai/internal/kie/callback', // Fixed to internal
95
- fallbackModel: 'FLUX_MAX',
96
- };
97
- } else if (resolvedModel.includes('flux-2/')) {
98
- body = {
99
- model: resolvedModel,
100
- input: {
101
- prompt,
102
- aspect_ratio: ratio,
103
- resolution,
104
- ...(hasReferences ? { input_urls: references } : {}),
105
- },
106
- };
107
- } else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
108
- body = {
109
- model: resolvedModel,
110
- input: {
111
- prompt,
112
- aspect_ratio: mapRatio(resolvedModel, ratio),
113
- quality,
114
- ...(hasReferences ? { image_urls: references } : {}),
115
- },
116
- };
117
- } else if (resolvedModel === 'nano-banana-pro') {
118
- body = {
119
- model: resolvedModel,
120
- input: {
121
- prompt,
122
- aspect_ratio: ratio,
123
- resolution,
124
- output_format: 'jpg', // ALWAYS JPG for Nano Banana Pro (lightweight)
125
- ...(hasReferences ? { image_input: references } : {}),
126
- },
127
- };
128
- } else if (resolvedModel.includes('google/nano-banana')) {
129
- body = {
130
- model: resolvedModel,
131
- input: {
132
- prompt,
133
- image_size: ratio,
134
- output_format: 'jpeg', // ALWAYS JPEG for Nano Banana (lightweight)
135
- ...(hasReferences ? { image_urls: references } : {}),
136
- },
137
- };
138
- } else if (resolvedModel === 'midjourney') {
139
- body = {
140
- taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
141
- speed: 'fast',
142
- prompt,
143
- fileUrls: references,
144
- aspectRatio: ratio,
145
- enableTranslation: true,
146
- };
147
- } else {
148
- // Default fallback (v4 edit style)
149
- body = {
150
- model: requestedModel === 'gpt4o-image' ? 'gpt-4o' : 'bytedance/seedream-v4-edit',
151
- input: {
152
- image_urls: references,
153
- prompt,
154
- image_size: mapRatio('seedream', ratio),
155
- },
156
- };
157
- }
84
+ if (resolvedModel === 'gpt4o-image') {
85
+ body = {
86
+ filesUrl: references,
87
+ prompt,
88
+ size: ratio,
89
+ callBackUrl: 'https://api.comandos.ai/internal/kie/callback',
90
+ fallbackModel: 'FLUX_MAX',
91
+ };
92
+ } else if (resolvedModel.includes('flux-2/')) {
93
+ body = {
94
+ model: resolvedModel,
95
+ input: {
96
+ prompt,
97
+ aspect_ratio: ratio,
98
+ resolution,
99
+ output_format: 'jpeg',
100
+ ...(hasReferences ? { input_urls: references } : {}),
101
+ },
102
+ };
103
+ } else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
104
+ body = {
105
+ model: resolvedModel,
106
+ input: {
107
+ prompt,
108
+ aspect_ratio: mapRatio(resolvedModel, ratio),
109
+ quality,
110
+ output_format: 'jpeg',
111
+ ...(hasReferences ? { image_urls: references } : {}),
112
+ },
113
+ };
114
+ } else if (resolvedModel === 'nano-banana-pro') {
115
+ body = {
116
+ model: resolvedModel,
117
+ input: {
118
+ prompt,
119
+ aspect_ratio: ratio,
120
+ resolution,
121
+ output_format: outputFormat || 'jpg', // User selection or fallback to jpg
122
+ ...(hasReferences ? { image_input: references } : {}),
123
+ },
124
+ };
125
+ } else if (resolvedModel.includes('google/nano-banana')) {
126
+ body = {
127
+ model: resolvedModel,
128
+ input: {
129
+ prompt,
130
+ image_size: ratio,
131
+ output_format: 'jpeg',
132
+ ...(hasReferences ? { image_urls: references } : {}),
133
+ },
134
+ };
135
+ } else if (resolvedModel === 'midjourney') {
136
+ body = {
137
+ taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
138
+ speed: 'fast',
139
+ prompt,
140
+ fileUrls: references,
141
+ aspectRatio: ratio,
142
+ enableTranslation: true,
143
+ };
144
+ } else {
145
+ body = {
146
+ model: requestedModel === 'gpt4o-image' ? 'gpt-4o' : 'bytedance/seedream-v4-edit',
147
+ input: {
148
+ image_urls: references,
149
+ prompt,
150
+ image_size: mapRatio('seedream', ratio),
151
+ output_format: 'jpeg',
152
+ },
153
+ };
154
+ }
158
155
 
159
- return {
160
- url: resolveGenerationUrl(resolvedModel),
161
- body,
162
- requestedModel,
163
- resolvedModel,
164
- ratio,
165
- references,
166
- hasReferences,
167
- };
156
+ return {
157
+ url: resolveGenerationUrl(resolvedModel),
158
+ body,
159
+ requestedModel,
160
+ resolvedModel,
161
+ ratio,
162
+ references,
163
+ hasReferences,
164
+ };
168
165
  };
169
166
 
170
167
  const extractReferences = (raw: unknown, model: string): string[] => {
171
- if (!raw || typeof raw !== 'object') {
172
- return [];
173
- }
174
- const collection = raw as { reference?: Array<{ url?: string }> };
175
- const items = Array.isArray(collection.reference) ? collection.reference : [];
176
-
177
- // Dynamic limit based on model docs
178
- let limit = 8;
179
- if (model === 'seedream') limit = 14;
180
- if (model === 'gpt4o-image') limit = 16;
181
- if (model.includes('nano-banana')) limit = 10;
182
-
183
- return items
184
- .map((entry) => String(entry?.url || '').trim())
185
- .filter((value) => value.length > 0 && /^https?:\/\//i.test(value))
186
- .slice(0, limit);
168
+ if (!raw || typeof raw !== 'object') return [];
169
+ const collection = raw as { reference?: Array<{ url?: string }> };
170
+ const items = Array.isArray(collection.reference) ? collection.reference : [];
171
+ let limit = 8;
172
+ if (model === 'seedream') limit = 14;
173
+ if (model === 'gpt4o-image') limit = 16;
174
+ if (model.includes('nano-banana')) limit = 10;
175
+ return items.map((entry) => String(entry?.url || '').trim()).filter((v) => v.length > 0 && /^https?:\/\//i.test(v)).slice(0, limit);
187
176
  };
188
177
 
189
178
  export class CommandosImage implements INodeType {
190
- description: INodeTypeDescription = {
191
- displayName: 'Commandos Image',
192
- name: 'commandosImage',
193
- group: ['transform'],
194
- version: 1,
195
- description: 'Create image tasks in Commandos API',
196
- defaults: {
197
- name: 'Commandos Image',
198
- },
199
- icon: 'file:Image.png',
200
- inputs: ['main'],
201
- outputs: ['main'],
202
- credentials: [
203
- {
204
- name: 'commandosApi',
205
- required: true,
206
- },
207
- ],
208
- properties: [
209
- {
210
- displayName: 'Operation',
211
- name: 'operation',
212
- type: 'options',
213
- noDataExpression: true,
214
- options: [
215
- {
216
- name: 'Create Task',
217
- value: 'create',
218
- },
219
- {
220
- name: 'Check Status',
221
- value: 'status',
222
- },
223
- ],
224
- default: 'create',
225
- },
226
- {
227
- displayName: 'Model',
228
- name: 'model',
229
- type: 'options',
230
- options: [
231
- { name: 'Seedream 4.5', value: 'seedream' },
232
- { name: 'Flux Pro', value: 'flux-pro' },
233
- { name: 'GPT-4o Image', value: 'gpt4o-image' },
234
- { name: 'Nano Banana', value: 'nano-banana' },
235
- { name: 'Nano Banana Pro', value: 'nano-banana-pro' },
236
- { name: 'Midjourney', value: 'midjourney' },
237
- ],
238
- default: 'seedream',
239
- displayOptions: {
240
- show: {
241
- operation: ['create'],
242
- },
243
- },
244
- },
245
- {
246
- displayName: 'Prompt',
247
- name: 'prompt',
248
- type: 'string',
249
- default: '',
250
- typeOptions: {
251
- rows: 4,
252
- },
253
- displayOptions: {
254
- show: {
255
- operation: ['create'],
256
- },
257
- },
258
- },
259
- {
260
- displayName: 'Ratio',
261
- name: 'ratio',
262
- type: 'options',
263
- options: [
264
- { name: '1:1', value: '1:1' },
265
- { name: '2:3', value: '2:3' },
266
- { name: '3:2', value: '3:2' },
267
- { name: '4:5', value: '4:5' },
268
- { name: '16:9', value: '16:9' },
269
- { name: '9:16', value: '9:16' },
270
- { name: '4:3', value: '4:3' },
271
- { name: '3:4', value: '3:4' },
272
- { name: '21:9', value: '21:9' },
273
- ],
274
- default: '2:3',
275
- displayOptions: {
276
- show: {
277
- operation: ['create'],
278
- },
279
- },
280
- },
281
- {
282
- displayName: 'Quality',
283
- name: 'quality',
284
- type: 'options',
285
- options: [
286
- { name: 'Basic (2K)', value: 'basic' },
287
- { name: 'High (4K)', value: 'high' },
288
- { name: 'Medium (Balanced)', value: 'medium' },
289
- ],
290
- default: 'basic',
291
- displayOptions: {
292
- show: {
293
- operation: ['create'],
294
- model: ['seedream', 'gpt4o-image', 'flux-pro'],
295
- },
296
- },
297
- },
298
- {
299
- displayName: 'Resolution',
300
- name: 'resolution',
301
- type: 'options',
302
- options: [
303
- { name: '1K', value: '1K' },
304
- { name: '2K', value: '2K' },
305
- { name: '4K', value: '4K' },
306
- ],
307
- default: '1K',
308
- displayOptions: {
309
- show: {
310
- operation: ['create'],
311
- model: ['flux-pro', 'nano-banana-pro'],
312
- },
313
- },
314
- },
315
- {
316
- displayName: 'References',
317
- name: 'references',
318
- type: 'fixedCollection',
319
- default: {},
320
- typeOptions: {
321
- multipleValues: true,
322
- },
323
- options: [
324
- {
325
- name: 'reference',
326
- displayName: 'Reference',
327
- values: [
328
- {
329
- displayName: 'Reference URL',
330
- name: 'url',
179
+ description: INodeTypeDescription = {
180
+ displayName: 'Commandos Image',
181
+ name: 'commandosImage',
182
+ group: ['transform'],
183
+ version: 1,
184
+ description: 'Create image tasks in Commandos API',
185
+ defaults: { name: 'Commandos Image' },
186
+ icon: 'file:Image.png',
187
+ inputs: ['main'],
188
+ outputs: ['main'],
189
+ credentials: [{ name: 'commandosApi', required: true }],
190
+ properties: [
191
+ {
192
+ displayName: 'Operation',
193
+ name: 'operation',
194
+ type: 'options',
195
+ noDataExpression: true,
196
+ options: [
197
+ { name: 'Create Task', value: 'create' },
198
+ { name: 'Check Status', value: 'status' },
199
+ ],
200
+ default: 'create',
201
+ },
202
+ {
203
+ displayName: 'Model',
204
+ name: 'model',
205
+ type: 'options',
206
+ options: [
207
+ { name: 'Seedream 4.5', value: 'seedream' },
208
+ { name: 'Flux Pro', value: 'flux-pro' },
209
+ { name: 'GPT-4o Image', value: 'gpt4o-image' },
210
+ { name: 'Nano Banana', value: 'nano-banana' },
211
+ { name: 'Nano Banana Pro', value: 'nano-banana-pro' },
212
+ { name: 'Midjourney', value: 'midjourney' },
213
+ ],
214
+ default: 'seedream',
215
+ displayOptions: { show: { operation: ['create'] } },
216
+ },
217
+ {
218
+ displayName: 'Prompt',
219
+ name: 'prompt',
220
+ type: 'string',
221
+ default: '',
222
+ typeOptions: { rows: 4 },
223
+ displayOptions: { show: { operation: ['create'] } },
224
+ },
225
+ {
226
+ displayName: 'Ratio',
227
+ name: 'ratio',
228
+ type: 'options',
229
+ options: [
230
+ { name: '1:1', value: '1:1' },
231
+ { name: '2:3', value: '2:3' },
232
+ { name: '3:2', value: '3:2' },
233
+ { name: '4:5', value: '4:5' },
234
+ { name: '16:9', value: '16:9' },
235
+ { name: '9:16', value: '9:16' },
236
+ { name: '4:3', value: '4:3' },
237
+ { name: '3:4', value: '3:4' },
238
+ { name: '21:9', value: '21:9' },
239
+ ],
240
+ default: '2:3',
241
+ displayOptions: {
242
+ show: { operation: ['create'] },
243
+ hide: { model: ['gpt4o-image'] },
244
+ },
245
+ },
246
+ {
247
+ displayName: 'Ratio',
248
+ name: 'ratioGpt',
249
+ type: 'options',
250
+ options: [
251
+ { name: '1:1', value: '1:1' },
252
+ { name: '2:3', value: '2:3' },
253
+ { name: '3:2', value: '3:2' },
254
+ ],
255
+ default: '2:3',
256
+ displayOptions: {
257
+ show: { operation: ['create'], model: ['gpt4o-image'] },
258
+ },
259
+ },
260
+ {
261
+ displayName: 'Output Format',
262
+ name: 'outputFormatBananaPro',
263
+ type: 'options',
264
+ options: [
265
+ { name: 'PNG', value: 'png' },
266
+ { name: 'JPG', value: 'jpg' },
267
+ ],
268
+ default: 'jpg',
269
+ displayOptions: {
270
+ show: { operation: ['create'], model: ['nano-banana-pro'] },
271
+ },
272
+ },
273
+ {
274
+ displayName: 'Quality',
275
+ name: 'quality',
276
+ type: 'options',
277
+ options: [
278
+ { name: 'Basic (2K)', value: 'basic' },
279
+ { name: 'High (4K)', value: 'high' },
280
+ { name: 'Medium (Balanced)', value: 'medium' },
281
+ ],
282
+ default: 'basic',
283
+ displayOptions: {
284
+ show: { operation: ['create'], model: ['seedream', 'gpt4o-image', 'flux-pro'] },
285
+ },
286
+ },
287
+ {
288
+ displayName: 'Resolution',
289
+ name: 'resolution',
290
+ type: 'options',
291
+ options: [
292
+ { name: '1K', value: '1K' },
293
+ { name: '2K', value: '2K' },
294
+ { name: '4K', value: '4K' },
295
+ ],
296
+ default: '1K',
297
+ displayOptions: {
298
+ show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
299
+ },
300
+ },
301
+ {
302
+ displayName: 'References',
303
+ name: 'references',
304
+ type: 'fixedCollection',
305
+ default: {},
306
+ typeOptions: { multipleValues: true },
307
+ options: [
308
+ {
309
+ name: 'reference',
310
+ displayName: 'Reference',
311
+ values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
312
+ },
313
+ ],
314
+ displayOptions: { show: { operation: ['create'] } },
315
+ },
316
+ {
317
+ displayName: 'Task ID',
318
+ name: 'taskId',
331
319
  type: 'string',
332
320
  default: '',
333
- },
334
- ],
335
- },
321
+ displayOptions: { show: { operation: ['status'] } },
322
+ },
336
323
  ],
337
- displayOptions: {
338
- show: {
339
- operation: ['create'],
340
- },
341
- },
342
- },
343
- {
344
- displayName: 'Task ID',
345
- name: 'taskId',
346
- type: 'string',
347
- default: '',
348
- displayOptions: {
349
- show: {
350
- operation: ['status'],
351
- },
352
- },
353
- },
354
- ],
355
- };
356
-
357
- async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
358
- const items = this.getInputData();
359
- const operation = this.getNodeParameter('operation', 0) as string;
360
- const credentials = await this.getCredentials('commandosApi');
361
- const licenseKey = String(credentials.licenseKey || '').trim();
362
-
363
- if (!licenseKey) {
364
- throw new NodeOperationError(this.getNode(), 'License key is required');
365
- }
366
-
367
- const results: INodeExecutionData[] = [];
368
-
369
- for (let i = 0; i < items.length; i += 1) {
370
- try {
371
- if (operation === 'create') {
372
- const model = String(this.getNodeParameter('model', i));
373
- const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
374
- const ratio = String(this.getNodeParameter('ratio', i));
375
- const references = extractReferences(this.getNodeParameter('references', i, {}), model);
376
-
377
- const quality = ['seedream', 'gpt4o-image', 'flux-pro'].includes(model)
378
- ? String(this.getNodeParameter('quality', i, 'basic'))
379
- : 'basic';
380
-
381
- const resolution = ['flux-pro', 'nano-banana-pro'].includes(model)
382
- ? String(this.getNodeParameter('resolution', i, '1K'))
383
- : '1K';
384
-
385
- if (!prompt) {
386
- throw new NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
387
- }
324
+ };
388
325
 
389
- const request = buildGenerationRequest({
390
- model,
391
- prompt,
392
- ratio,
393
- references,
394
- quality,
395
- resolution
396
- });
397
-
398
- if (!request.url) {
399
- throw new NodeOperationError(
400
- this.getNode(),
401
- 'URL генерации не настроен. Проверь переменные окружения COMMANDOS_IMAGE_URL_DEFAULT/COMMANDOS_IMAGE_URL_GPT4O/COMMANDOS_IMAGE_URL_MJ',
402
- { itemIndex: i },
403
- );
404
- }
326
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
327
+ const items = this.getInputData();
328
+ const operation = this.getNodeParameter('operation', 0) as string;
329
+ const credentials = await this.getCredentials('commandosApi');
330
+ const licenseKey = String(credentials.licenseKey || '').trim();
331
+ if (!licenseKey) throw new NodeOperationError(this.getNode(), 'License key is required');
332
+ const results: INodeExecutionData[] = [];
333
+ for (let i = 0; i < items.length; i += 1) {
334
+ try {
335
+ if (operation === 'create') {
336
+ const model = String(this.getNodeParameter('model', i));
337
+ const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
405
338
 
406
- const payload = {
407
- url: request.url,
408
- body: request.body,
409
- requestedModel: request.requestedModel,
410
- resolvedModel: request.resolvedModel,
411
- ratio: request.ratio,
412
- references: request.references,
413
- hasReferences: request.hasReferences,
414
- _v: '0.1.8'
415
- };
339
+ let ratio = '';
340
+ if (model === 'gpt4o-image') {
341
+ ratio = String(this.getNodeParameter('ratioGpt', i));
342
+ } else {
343
+ ratio = String(this.getNodeParameter('ratio', i));
344
+ }
416
345
 
417
- const response = await this.helpers.request({
418
- method: 'POST',
419
- url: `${COMMANDOS_API_URL}/tasks`,
420
- headers: {
421
- 'Content-Type': 'application/json',
422
- 'X-License-Key': licenseKey,
423
- },
424
- body: {
425
- process_type: 'IMAGE_GENERATION',
426
- payload,
427
- },
428
- json: true,
429
- });
346
+ const references = extractReferences(this.getNodeParameter('references', i, {}), model);
347
+ const quality = ['seedream', 'gpt4o-image', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
348
+ const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
349
+ const outputFormat = (model === 'nano-banana-pro') ? String(this.getNodeParameter('outputFormatBananaPro', i, 'jpg')) : undefined;
430
350
 
431
- results.push({ json: response as IDataObject });
432
- } else if (operation === 'status') {
433
- const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
434
- if (!taskId) {
435
- throw new NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
436
- }
351
+ if (!prompt) throw new NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
437
352
 
438
- const response = await this.helpers.request({
439
- method: 'GET',
440
- url: `${COMMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
441
- headers: {
442
- 'X-License-Key': licenseKey,
443
- },
444
- json: true,
445
- });
446
-
447
- results.push({ json: response as IDataObject });
448
- }
449
- } catch (error) {
450
- if (error instanceof NodeOperationError) {
451
- throw error;
353
+ const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution, outputFormat });
354
+ const payload = { ...request, licenseKey, _v: '0.1.10' };
355
+ const response = await this.helpers.request({
356
+ method: 'POST',
357
+ url: `${COMMANDOS_API_URL}/tasks`,
358
+ headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
359
+ body: { process_type: 'IMAGE_GENERATION', payload },
360
+ json: true,
361
+ });
362
+ results.push({ json: response as IDataObject });
363
+ } else if (operation === 'status') {
364
+ const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
365
+ if (!taskId) throw new NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
366
+ const response = await this.helpers.request({
367
+ method: 'GET',
368
+ url: `${COMMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
369
+ headers: { 'X-License-Key': licenseKey },
370
+ json: true,
371
+ });
372
+ results.push({ json: response as IDataObject });
373
+ }
374
+ } catch (error) {
375
+ if (error instanceof NodeOperationError) throw error;
376
+ const message = error instanceof Error ? error.message : 'Request failed';
377
+ throw new NodeOperationError(this.getNode(), message, { itemIndex: i });
378
+ }
452
379
  }
453
- const message = error instanceof Error ? error.message : 'Request failed';
454
- throw new NodeOperationError(this.getNode(), message, { itemIndex: i });
455
- }
380
+ return [results];
456
381
  }
457
-
458
- return [results];
459
- }
460
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-commandos-image",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Commandos Image custom node",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",