n8n-nodes-commandos-image 0.1.4

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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "note": "Example payload template for Commandos Image."
3
+ }
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { CommandosImage } from './nodes/CommandosImage.node';
2
+ import { CommandosApi } from './credentials/CommandosApi.credentials';
3
+
4
+ export { CommandosImage, CommandosApi };
@@ -0,0 +1,368 @@
1
+ import type {
2
+ IDataObject,
3
+ IExecuteFunctions,
4
+ INodeExecutionData,
5
+ INodeType,
6
+ INodeTypeDescription,
7
+ } from 'n8n-workflow';
8
+ import { NodeOperationError } from 'n8n-workflow';
9
+
10
+ const COMMANDOS_API_URL = 'https://api.comandos.ai';
11
+ const GENERATION_URL_DEFAULT = String(process.env.COMMANDOS_IMAGE_URL_DEFAULT || '').trim();
12
+ const GENERATION_URL_GPT4O = String(process.env.COMMANDOS_IMAGE_URL_GPT4O || '').trim();
13
+ const GENERATION_URL_MJ = String(process.env.COMMANDOS_IMAGE_URL_MJ || '').trim();
14
+
15
+ type BuildParams = {
16
+ model: string;
17
+ prompt: string;
18
+ ratio: string;
19
+ references: string[];
20
+ };
21
+
22
+ type BuildResult = {
23
+ url: string;
24
+ body: Record<string, unknown>;
25
+ requestedModel: string;
26
+ resolvedModel: string;
27
+ ratio: string;
28
+ references: string[];
29
+ hasReferences: boolean;
30
+ };
31
+
32
+ const resolveGenerationUrl = (resolvedModel: string): string => {
33
+ if (resolvedModel === 'gpt4o-image') {
34
+ return GENERATION_URL_GPT4O || GENERATION_URL_DEFAULT;
35
+ }
36
+ if (resolvedModel === 'midjourney') {
37
+ return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
38
+ }
39
+ return GENERATION_URL_DEFAULT;
40
+ };
41
+
42
+ const buildGenerationRequest = ({ model, prompt, ratio, references }: BuildParams): BuildResult => {
43
+ const hasReferences = references.length > 0;
44
+ const requestedModel = model;
45
+ let resolvedModel = model;
46
+
47
+ if (requestedModel === 'flux-pro') {
48
+ resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
49
+ } else if (requestedModel === 'nano-banana') {
50
+ resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
51
+ } else if (requestedModel === 'nano-banana-pro') {
52
+ resolvedModel = 'nano-banana-pro';
53
+ } else if (requestedModel === 'seedream') {
54
+ resolvedModel = 'bytedance/seedream-v4-edit';
55
+ }
56
+
57
+ if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
58
+ if (requestedModel !== 'midjourney') {
59
+ resolvedModel = 'gpt4o-image';
60
+ }
61
+ }
62
+
63
+ let body: Record<string, unknown> = {};
64
+
65
+ if (resolvedModel === 'gpt4o-image') {
66
+ body = {
67
+ filesUrl: references,
68
+ prompt,
69
+ size: ratio,
70
+ callBackUrl: 'https://your-callback-url.com/callback',
71
+ fallbackModel: 'FLUX_MAX',
72
+ };
73
+ } else if (resolvedModel.includes('flux-2/')) {
74
+ body = {
75
+ model: resolvedModel,
76
+ input: {
77
+ prompt,
78
+ aspect_ratio: ratio,
79
+ resolution: '1K',
80
+ ...(hasReferences ? { input_urls: references } : {}),
81
+ },
82
+ };
83
+ } else if (resolvedModel === 'nano-banana-pro') {
84
+ body = {
85
+ model: resolvedModel,
86
+ input: {
87
+ prompt,
88
+ aspect_ratio: ratio,
89
+ resolution: '1K',
90
+ output_format: 'png',
91
+ ...(hasReferences ? { image_input: references } : {}),
92
+ },
93
+ };
94
+ } else if (resolvedModel.includes('google/nano-banana')) {
95
+ body = {
96
+ model: resolvedModel,
97
+ input: {
98
+ prompt,
99
+ image_size: ratio,
100
+ output_format: 'png',
101
+ ...(hasReferences ? { image_urls: references } : {}),
102
+ },
103
+ };
104
+ } else if (resolvedModel === 'midjourney') {
105
+ body = {
106
+ taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
107
+ speed: 'fast',
108
+ prompt,
109
+ fileUrls: references,
110
+ aspectRatio: ratio,
111
+ enableTranslation: true,
112
+ };
113
+ } else {
114
+ body = {
115
+ model: 'bytedance/seedream-v4-edit',
116
+ input: {
117
+ image_urls: references,
118
+ prompt,
119
+ image_size: ratio === '2:3' ? 'portrait_3_2' : ratio,
120
+ image_resolution: '1K',
121
+ },
122
+ };
123
+ }
124
+
125
+ return {
126
+ url: resolveGenerationUrl(resolvedModel),
127
+ body,
128
+ requestedModel,
129
+ resolvedModel,
130
+ ratio,
131
+ references,
132
+ hasReferences,
133
+ };
134
+ };
135
+
136
+ const extractReferences = (raw: unknown): string[] => {
137
+ if (!raw || typeof raw !== 'object') {
138
+ return [];
139
+ }
140
+ const collection = raw as { reference?: Array<{ url?: string }> };
141
+ const items = Array.isArray(collection.reference) ? collection.reference : [];
142
+ return items
143
+ .map((entry) => String(entry?.url || '').trim())
144
+ .filter((value) => value.length > 0 && /^https?:\/\//i.test(value))
145
+ .slice(0, 2);
146
+ };
147
+
148
+ export class CommandosImage implements INodeType {
149
+ description: INodeTypeDescription = {
150
+ displayName: 'Commandos Image',
151
+ name: 'commandosImage',
152
+ group: ['transform'],
153
+ version: 1,
154
+ description: 'Create image tasks in Commandos API',
155
+ defaults: {
156
+ name: 'Commandos Image',
157
+ },
158
+ icon: 'file:Image.png',
159
+ inputs: ['main'],
160
+ outputs: ['main'],
161
+ credentials: [
162
+ {
163
+ name: 'commandosApi',
164
+ required: true,
165
+ },
166
+ ],
167
+ properties: [
168
+ {
169
+ displayName: 'Operation',
170
+ name: 'operation',
171
+ type: 'options',
172
+ noDataExpression: true,
173
+ options: [
174
+ {
175
+ name: 'Create Task',
176
+ value: 'create',
177
+ },
178
+ {
179
+ name: 'Check Status',
180
+ value: 'status',
181
+ },
182
+ ],
183
+ default: 'create',
184
+ },
185
+ {
186
+ displayName: 'Model',
187
+ name: 'model',
188
+ type: 'options',
189
+ options: [
190
+ { name: 'Flux Pro', value: 'flux-pro' },
191
+ { name: 'GPT-4o Image', value: 'gpt4o-image' },
192
+ { name: 'Nano Banana', value: 'nano-banana' },
193
+ { name: 'Nano Banana Pro', value: 'nano-banana-pro' },
194
+ { name: 'Seedream', value: 'seedream' },
195
+ { name: 'Midjourney', value: 'midjourney' },
196
+ ],
197
+ default: 'flux-pro',
198
+ displayOptions: {
199
+ show: {
200
+ operation: ['create'],
201
+ },
202
+ },
203
+ },
204
+ {
205
+ displayName: 'Prompt',
206
+ name: 'prompt',
207
+ type: 'string',
208
+ default: '',
209
+ typeOptions: {
210
+ rows: 4,
211
+ },
212
+ displayOptions: {
213
+ show: {
214
+ operation: ['create'],
215
+ },
216
+ },
217
+ },
218
+ {
219
+ displayName: 'Ratio',
220
+ name: 'ratio',
221
+ type: 'options',
222
+ options: [
223
+ { name: '1:1', value: '1:1' },
224
+ { name: '2:3', value: '2:3' },
225
+ { name: '3:2', value: '3:2' },
226
+ { name: '4:5', value: '4:5' },
227
+ { name: '16:9', value: '16:9' },
228
+ { name: '9:16', value: '9:16' },
229
+ ],
230
+ default: '2:3',
231
+ displayOptions: {
232
+ show: {
233
+ operation: ['create'],
234
+ },
235
+ },
236
+ },
237
+ {
238
+ displayName: 'References',
239
+ name: 'references',
240
+ type: 'fixedCollection',
241
+ default: {},
242
+ typeOptions: {
243
+ multipleValues: true,
244
+ },
245
+ options: [
246
+ {
247
+ name: 'reference',
248
+ displayName: 'Reference',
249
+ values: [
250
+ {
251
+ displayName: 'Reference URL',
252
+ name: 'url',
253
+ type: 'string',
254
+ default: '',
255
+ },
256
+ ],
257
+ },
258
+ ],
259
+ displayOptions: {
260
+ show: {
261
+ operation: ['create'],
262
+ },
263
+ },
264
+ },
265
+ {
266
+ displayName: 'Task ID',
267
+ name: 'taskId',
268
+ type: 'string',
269
+ default: '',
270
+ displayOptions: {
271
+ show: {
272
+ operation: ['status'],
273
+ },
274
+ },
275
+ },
276
+ ],
277
+ };
278
+
279
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
280
+ const items = this.getInputData();
281
+ const operation = this.getNodeParameter('operation', 0) as string;
282
+ const credentials = await this.getCredentials('commandosApi');
283
+ const licenseKey = String(credentials.licenseKey || '').trim();
284
+
285
+ if (!licenseKey) {
286
+ throw new NodeOperationError(this.getNode(), 'License key is required');
287
+ }
288
+
289
+ const results: INodeExecutionData[] = [];
290
+
291
+ for (let i = 0; i < items.length; i += 1) {
292
+ try {
293
+ if (operation === 'create') {
294
+ const model = String(this.getNodeParameter('model', i));
295
+ const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
296
+ const ratio = String(this.getNodeParameter('ratio', i));
297
+ const references = extractReferences(this.getNodeParameter('references', i, {}));
298
+
299
+ if (!prompt) {
300
+ throw new NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
301
+ }
302
+
303
+ const request = buildGenerationRequest({ model, prompt, ratio, references });
304
+ if (!request.url) {
305
+ throw new NodeOperationError(
306
+ this.getNode(),
307
+ 'URL генерации не настроен. Проверь переменные окружения COMMANDOS_IMAGE_URL_DEFAULT/COMMANDOS_IMAGE_URL_GPT4O/COMMANDOS_IMAGE_URL_MJ',
308
+ { itemIndex: i },
309
+ );
310
+ }
311
+
312
+ const payload = {
313
+ url: request.url,
314
+ body: request.body,
315
+ requestedModel: request.requestedModel,
316
+ resolvedModel: request.resolvedModel,
317
+ ratio: request.ratio,
318
+ references: request.references,
319
+ hasReferences: request.hasReferences,
320
+ };
321
+
322
+ const response = await this.helpers.request({
323
+ method: 'POST',
324
+ url: `${COMMANDOS_API_URL}/tasks`,
325
+ headers: {
326
+ 'Content-Type': 'application/json',
327
+ 'X-License-Key': licenseKey,
328
+ },
329
+ body: {
330
+ process_type: 'IMAGE_GENERATION',
331
+ payload,
332
+ },
333
+ json: true,
334
+ });
335
+
336
+ results.push({ json: response as IDataObject });
337
+ } else if (operation === 'status') {
338
+ const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
339
+ if (!taskId) {
340
+ throw new NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
341
+ }
342
+
343
+ const response = await this.helpers.request({
344
+ method: 'GET',
345
+ url: `${COMMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
346
+ headers: {
347
+ 'X-License-Key': licenseKey,
348
+ },
349
+ json: true,
350
+ });
351
+
352
+ results.push({ json: response as IDataObject });
353
+ } else {
354
+ throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, {
355
+ itemIndex: i,
356
+ });
357
+ }
358
+ } catch (error) {
359
+ if (error instanceof NodeOperationError) {
360
+ throw error;
361
+ }
362
+ throw new NodeOperationError(this.getNode(), 'Request failed', { itemIndex: i });
363
+ }
364
+ }
365
+
366
+ return [results];
367
+ }
368
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "n8n-nodes-commandos-image",
3
+ "version": "0.1.4",
4
+ "description": "Commandos Image custom node",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json && mkdir -p dist/nodes && cp -f Image.png dist/nodes/"
9
+ },
10
+ "dependencies": {
11
+ "n8n-workflow": "^1.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^18.19.0",
15
+ "typescript": "^5.4.5"
16
+ },
17
+ "n8n": {
18
+ "nodes": [
19
+ "dist/nodes/CommandosImage.node.js"
20
+ ],
21
+ "credentials": [
22
+ "dist/credentials/CommandosApi.credentials.js"
23
+ ]
24
+ }
25
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2019"],
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": ".",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "resolveJsonModule": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["nodes/**/*.ts", "credentials/**/*.ts", "index.ts"]
15
+ }