@comandosai/n8n-nodes-media 0.1.2

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/Media.png ADDED
Binary file
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Comandos Media (кастомная нода n8n)
2
+
3
+ Кастомная нода для создания задач генерации изображений и видео через Commandos API.
4
+
5
+ ## Где находится
6
+
7
+ - Код ноды: `/root/sandbox/nodes/image`
8
+ - Пакет: `@comandosai/n8n-nodes-media`
9
+
10
+
11
+ ## Требования
12
+
13
+ - n8n (dev окружение)
14
+ - Node.js 18+
15
+ - Доступ к Commandos API
16
+
17
+ ## Подключение в n8n-dev
18
+
19
+ В `docker-compose` уже настроено:
20
+
21
+ - монтирование `/root/sandbox/nodes` в `/custom/commandos`
22
+ - переменная `N8N_CUSTOM_EXTENSIONS=/custom/commandos`
23
+
24
+ Если переносите в другое окружение, убедитесь, что путь задан корректно и n8n перезапущен.
25
+
26
+ ## Credentials
27
+
28
+ Тип: `Commandos API`
29
+
30
+ Поля:
31
+ - `License Key` — передаётся как `X-License-Key`.
32
+
33
+ ## Операции
34
+
35
+ ### Create Task
36
+
37
+ Создаёт задачу в API (`POST /tasks`, `process_type=IMAGE_GENERATION`). Нода сама формирует:
38
+
39
+ - `url` для генерации
40
+ - `body` для выбранной модели
41
+
42
+ Входные поля:
43
+ - `Model`: Flux Pro, GPT-4o Image, Nano Banana, Nano Banana Pro, Seedream, Midjourney
44
+ - `Prompt`
45
+ - `Ratio`: `1:1`, `2:3`, `3:2`, `4:5`, `16:9`, `9:16`
46
+ - `References`: до 2 URL (опционально)
47
+
48
+ Выход:
49
+ - `taskId`, `status`, `pollUrl`
50
+
51
+ ### Check Status
52
+
53
+ Запрашивает статус по `taskId` (`GET /tasks/:taskId`).
54
+
55
+ ## Базовый URL API
56
+
57
+ Нода всегда использует `https://api.comandos.ai`.
58
+
59
+ ## Переменные окружения (URL генерации)
60
+
61
+ Нода берёт URL генерации из переменных окружения:
62
+
63
+ - `COMMANDOS_IMAGE_URL_DEFAULT`
64
+ - `COMMANDOS_IMAGE_URL_GPT4O` (опционально)
65
+ - `COMMANDOS_IMAGE_URL_MJ` (опционально)
66
+
67
+ Если специальные URL не заданы, используется `COMMANDOS_IMAGE_URL_DEFAULT`.
68
+
69
+ ## Пример workflow
70
+
71
+ 1. **Commandos Image** → `Create Task`
72
+ 2. **Wait** (20–60 секунд)
73
+ 3. **Commandos Image** → `Check Status` (`{{$json.taskId}}`)
74
+
75
+ ## Сборка
76
+
77
+ ```bash
78
+ npm install
79
+ npm run build
80
+ ```
81
+
82
+ После изменений всегда пересобирайте `dist` и перезапускайте n8n.
83
+
84
+ ## Иконка
85
+
86
+ Файл иконки: `Image.png` (копируется в `dist/nodes` при сборке).
@@ -0,0 +1,29 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+
3
+ export class ComandosApi implements ICredentialType {
4
+ name = 'comandosApi';
5
+ displayName = 'Comandos API';
6
+ documentationUrl = 'https://api.comandos.ai';
7
+ properties: INodeProperties[] = [
8
+ {
9
+ displayName: 'License Key',
10
+ name: 'licenseKey',
11
+ type: 'string',
12
+ default: '',
13
+ required: true,
14
+ typeOptions: {
15
+ password: true,
16
+ },
17
+ },
18
+ {
19
+ displayName: 'Kie.ai API Key',
20
+ name: 'kieApiKey',
21
+ type: 'string',
22
+ default: '',
23
+ required: false,
24
+ typeOptions: {
25
+ password: true,
26
+ },
27
+ },
28
+ ];
29
+ }
@@ -0,0 +1,7 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class ComandosApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComandosApi = void 0;
4
+ class ComandosApi {
5
+ constructor() {
6
+ this.name = 'comandosApi';
7
+ this.displayName = 'Comandos API';
8
+ this.documentationUrl = 'https://api.comandos.ai';
9
+ this.properties = [
10
+ {
11
+ displayName: 'License Key',
12
+ name: 'licenseKey',
13
+ type: 'string',
14
+ default: '',
15
+ required: true,
16
+ typeOptions: {
17
+ password: true,
18
+ },
19
+ },
20
+ {
21
+ displayName: 'Kie.ai API Key',
22
+ name: 'kieApiKey',
23
+ type: 'string',
24
+ default: '',
25
+ required: false,
26
+ typeOptions: {
27
+ password: true,
28
+ },
29
+ },
30
+ ];
31
+ }
32
+ }
33
+ exports.ComandosApi = ComandosApi;
@@ -0,0 +1,3 @@
1
+ import { ComandosMedia } from './nodes/ComandosMedia.node';
2
+ import { ComandosApi } from './credentials/ComandosApi.credentials';
3
+ export { ComandosMedia, ComandosApi };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComandosApi = exports.ComandosMedia = void 0;
4
+ const ComandosMedia_node_1 = require("./nodes/ComandosMedia.node");
5
+ Object.defineProperty(exports, "ComandosMedia", { enumerable: true, get: function () { return ComandosMedia_node_1.ComandosMedia; } });
6
+ const ComandosApi_credentials_1 = require("./credentials/ComandosApi.credentials");
7
+ Object.defineProperty(exports, "ComandosApi", { enumerable: true, get: function () { return ComandosApi_credentials_1.ComandosApi; } });
@@ -0,0 +1,9 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class ComandosImage implements INodeType {
3
+ displayName: 'Comandos Media';
4
+ name: 'comandosMedia';
5
+ group: ['transform'];
6
+ version: 1;
7
+ description: INodeTypeDescription;
8
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
9
+ }
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComandosImage = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const COMANDOS_API_URL = String(process.env.COMANDOS_API_BASE_URL || process.env.COMMANDOS_API_BASE_URL || "https://api.comandos.ai").trim();
6
+ const GENERATION_URL_DEFAULT = String(process.env.COMANDOS_IMAGE_URL_DEFAULT || process.env.COMMANDOS_IMAGE_URL_DEFAULT || "").trim();
7
+ const GENERATION_URL_MJ = String(process.env.COMANDOS_IMAGE_URL_MJ || process.env.COMMANDOS_IMAGE_URL_MJ || "").trim();
8
+ const resolveGenerationUrl = (resolvedModel) => {
9
+ if (resolvedModel === 'midjourney') {
10
+ return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
11
+ }
12
+ return GENERATION_URL_DEFAULT;
13
+ };
14
+ const mapRatio = (model, ratio) => {
15
+ const isSeedream = model.includes('seedream');
16
+ if (isSeedream) {
17
+ if (ratio === '2:3')
18
+ return 'portrait_3_2';
19
+ if (ratio === '3:2')
20
+ return 'landscape_2_3';
21
+ }
22
+ return ratio;
23
+ };
24
+ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'basic', resolution = '1K', }) => {
25
+ const hasReferences = references.length > 0;
26
+ const requestedModel = model;
27
+ let resolvedModel = model;
28
+ if (requestedModel === 'flux-pro') {
29
+ resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
30
+ }
31
+ else if (requestedModel === 'nano-banana') {
32
+ resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
33
+ }
34
+ else if (requestedModel === 'nano-banana-pro') {
35
+ resolvedModel = 'nano-banana-pro';
36
+ }
37
+ else if (requestedModel === 'seedream') {
38
+ resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
39
+ }
40
+ if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
41
+ if (requestedModel === 'midjourney') {
42
+ resolvedModel = 'midjourney';
43
+ }
44
+ }
45
+ let body = {};
46
+ if (resolvedModel.includes('flux-2/')) {
47
+ body = {
48
+ model: resolvedModel,
49
+ input: {
50
+ prompt,
51
+ aspect_ratio: ratio,
52
+ resolution,
53
+ output_format: 'jpeg', // Force lightweight
54
+ ...(hasReferences ? { input_urls: references } : {}),
55
+ },
56
+ };
57
+ }
58
+ else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
59
+ body = {
60
+ model: resolvedModel,
61
+ input: {
62
+ prompt,
63
+ aspect_ratio: mapRatio(resolvedModel, ratio),
64
+ quality,
65
+ output_format: 'jpeg', // Force lightweight
66
+ ...(hasReferences ? { image_urls: references } : {}),
67
+ },
68
+ };
69
+ }
70
+ else if (resolvedModel === 'nano-banana-pro') {
71
+ body = {
72
+ model: resolvedModel,
73
+ input: {
74
+ prompt,
75
+ aspect_ratio: ratio,
76
+ resolution,
77
+ output_format: 'jpg', // Force lightweight
78
+ ...(hasReferences ? { image_input: references } : {}),
79
+ },
80
+ };
81
+ }
82
+ else if (resolvedModel.includes('google/nano-banana')) {
83
+ body = {
84
+ model: resolvedModel,
85
+ input: {
86
+ prompt,
87
+ image_size: ratio,
88
+ output_format: 'jpeg', // Force lightweight
89
+ ...(hasReferences ? { image_urls: references } : {}),
90
+ },
91
+ };
92
+ }
93
+ else if (resolvedModel === 'midjourney') {
94
+ body = {
95
+ taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
96
+ speed: 'fast',
97
+ prompt,
98
+ fileUrls: references,
99
+ aspectRatio: ratio,
100
+ enableTranslation: true,
101
+ };
102
+ }
103
+ else {
104
+ body = {
105
+ model: 'bytedance/seedream-v4-edit',
106
+ input: {
107
+ image_urls: references,
108
+ prompt,
109
+ image_size: mapRatio('seedream', ratio),
110
+ output_format: 'jpeg',
111
+ },
112
+ };
113
+ }
114
+ return {
115
+ url: resolveGenerationUrl(resolvedModel),
116
+ body,
117
+ requestedModel,
118
+ resolvedModel,
119
+ ratio,
120
+ references,
121
+ hasReferences,
122
+ };
123
+ };
124
+ const extractReferences = (raw, model) => {
125
+ if (!raw || typeof raw !== 'object')
126
+ return [];
127
+ const collection = raw;
128
+ const items = Array.isArray(collection.reference) ? collection.reference : [];
129
+ let limit = 8;
130
+ if (model === 'seedream')
131
+ limit = 14;
132
+ if (model.includes('nano-banana'))
133
+ limit = 10;
134
+ 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);
135
+ };
136
+ class ComandosImage {
137
+ constructor() {
138
+ this.description = {
139
+ displayName: 'Comandos Media',
140
+ name: 'comandosMedia',
141
+ group: ['transform'],
142
+ version: 1,
143
+ description: 'Create image or video tasks in Comandos API',
144
+ defaults: { name: 'Comandos Image' },
145
+ icon: 'file:Image.png',
146
+ inputs: ['main'],
147
+ outputs: ['main'],
148
+ credentials: [{ name: 'comandosApi', required: true }],
149
+ properties: [
150
+ {
151
+ displayName: 'Operation',
152
+ name: 'operation',
153
+ type: 'options',
154
+ noDataExpression: true,
155
+ options: [
156
+ { name: 'Create Task', value: 'create' },
157
+ { name: 'Check Status', value: 'status' },
158
+ ],
159
+ default: 'create',
160
+ },
161
+ {
162
+ displayName: 'Model',
163
+ name: 'model',
164
+ type: 'options',
165
+ options: [
166
+ { name: 'Seedream 4.5', value: 'seedream' },
167
+ { name: 'Flux Pro', value: 'flux-pro' },
168
+ { name: 'Nano Banana', value: 'nano-banana' },
169
+ { name: 'Nano Banana Pro', value: 'nano-banana-pro' },
170
+ { name: 'Midjourney', value: 'midjourney' },
171
+ ],
172
+ default: 'seedream',
173
+ displayOptions: { show: { operation: ['create'] } },
174
+ },
175
+ {
176
+ displayName: 'Prompt',
177
+ name: 'prompt',
178
+ type: 'string',
179
+ default: '',
180
+ typeOptions: { rows: 4 },
181
+ displayOptions: { show: { operation: ['create'] } },
182
+ },
183
+ {
184
+ displayName: 'Ratio',
185
+ name: 'ratio',
186
+ type: 'options',
187
+ options: [
188
+ { name: '1:1', value: '1:1' },
189
+ { name: '2:3', value: '2:3' },
190
+ { name: '3:2', value: '3:2' },
191
+ { name: '4:5', value: '4:5' },
192
+ { name: '16:9', value: '16:9' },
193
+ { name: '9:16', value: '9:16' },
194
+ { name: '4:3', value: '4:3' },
195
+ { name: '3:4', value: '3:4' },
196
+ { name: '21:9', value: '21:9' },
197
+ ],
198
+ default: '2:3',
199
+ displayOptions: {
200
+ show: { operation: ['create'] },
201
+ },
202
+ },
203
+ {
204
+ displayName: 'Quality',
205
+ name: 'quality',
206
+ type: 'options',
207
+ options: [
208
+ { name: 'Basic (2K)', value: 'basic' },
209
+ { name: 'High (4K)', value: 'high' },
210
+ { name: 'Medium (Balanced)', value: 'medium' },
211
+ ],
212
+ default: 'basic',
213
+ displayOptions: {
214
+ show: { operation: ['create'], model: ['seedream', 'flux-pro'] },
215
+ },
216
+ },
217
+ {
218
+ displayName: 'Resolution',
219
+ name: 'resolution',
220
+ type: 'options',
221
+ options: [
222
+ { name: '1K', value: '1K' },
223
+ { name: '2K', value: '2K' },
224
+ { name: '4K', value: '4K' },
225
+ ],
226
+ default: '1K',
227
+ displayOptions: {
228
+ show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
229
+ },
230
+ },
231
+ {
232
+ displayName: 'References',
233
+ name: 'references',
234
+ type: 'fixedCollection',
235
+ default: {},
236
+ typeOptions: { multipleValues: true },
237
+ options: [
238
+ {
239
+ name: 'reference',
240
+ displayName: 'Reference',
241
+ values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
242
+ },
243
+ ],
244
+ displayOptions: { show: { operation: ['create'] } },
245
+ },
246
+ {
247
+ displayName: 'Task ID',
248
+ name: 'taskId',
249
+ type: 'string',
250
+ default: '',
251
+ displayOptions: { show: { operation: ['status'] } },
252
+ },
253
+ ],
254
+ };
255
+ }
256
+ async execute() {
257
+ const items = this.getInputData();
258
+ const operation = this.getNodeParameter('operation', 0);
259
+ const credentials = await this.getCredentials('comandosApi');
260
+ const licenseKey = String(credentials.licenseKey || '').trim();
261
+ const kieApiKey = String(credentials.kieApiKey || '').trim();
262
+ if (!licenseKey)
263
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'License key is required');
264
+ const results = [];
265
+ for (let i = 0; i < items.length; i += 1) {
266
+ try {
267
+ if (operation === 'create') {
268
+ const model = String(this.getNodeParameter('model', i));
269
+ const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
270
+ const ratio = String(this.getNodeParameter('ratio', i));
271
+ const references = extractReferences(this.getNodeParameter('references', i, {}), model);
272
+ const quality = ['seedream', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
273
+ const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
274
+ if (!prompt)
275
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
276
+ const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution });
277
+ const payload = { ...request, licenseKey, _v: '0.1.0' };
278
+ const response = await this.helpers.request({
279
+ method: 'POST',
280
+ url: `${COMANDOS_API_URL}/tasks`,
281
+ headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
282
+ body: { process_type: 'IMAGE_GENERATION', payload },
283
+ json: true,
284
+ });
285
+ results.push({ json: response });
286
+ }
287
+ else if (operation === 'status') {
288
+ const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
289
+ if (!taskId)
290
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
291
+ const response = await this.helpers.request({
292
+ method: 'GET',
293
+ url: `${COMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
294
+ headers: { 'X-License-Key': licenseKey },
295
+ json: true,
296
+ });
297
+ results.push({ json: response });
298
+ }
299
+ }
300
+ catch (error) {
301
+ if (error instanceof n8n_workflow_1.NodeOperationError)
302
+ throw error;
303
+ const message = error instanceof Error ? error.message : 'Request failed';
304
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), message, { itemIndex: i });
305
+ }
306
+ }
307
+ return [results];
308
+ }
309
+ }
310
+ exports.ComandosImage = ComandosImage;
@@ -0,0 +1,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class ComandosMedia implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComandosMedia = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const COMANDOS_API_URL = String(process.env.COMANDOS_API_BASE_URL || process.env.COMMANDOS_API_BASE_URL || "https://api.comandos.ai").trim();
6
+ const GENERATION_URL_DEFAULT = String(process.env.COMANDOS_IMAGE_URL_DEFAULT || process.env.COMMANDOS_IMAGE_URL_DEFAULT || "").trim();
7
+ const GENERATION_URL_MJ = String(process.env.COMANDOS_IMAGE_URL_MJ || process.env.COMMANDOS_IMAGE_URL_MJ || "").trim();
8
+ const resolveGenerationUrl = (resolvedModel) => {
9
+ if (resolvedModel === 'midjourney') {
10
+ return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
11
+ }
12
+ return GENERATION_URL_DEFAULT;
13
+ };
14
+ const mapRatio = (model, ratio) => {
15
+ const isSeedream = model.includes('seedream');
16
+ if (isSeedream) {
17
+ if (ratio === '2:3')
18
+ return 'portrait_3_2';
19
+ if (ratio === '3:2')
20
+ return 'landscape_2_3';
21
+ }
22
+ return ratio;
23
+ };
24
+ const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'basic', resolution = '1K', }) => {
25
+ const hasReferences = references.length > 0;
26
+ const requestedModel = model;
27
+ let resolvedModel = model;
28
+ if (requestedModel === 'flux-pro') {
29
+ resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
30
+ }
31
+ else if (requestedModel === 'nano-banana') {
32
+ resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
33
+ }
34
+ else if (requestedModel === 'nano-banana-pro') {
35
+ resolvedModel = 'nano-banana-pro';
36
+ }
37
+ else if (requestedModel === 'seedream') {
38
+ resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
39
+ }
40
+ if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
41
+ if (requestedModel === 'midjourney') {
42
+ resolvedModel = 'midjourney';
43
+ }
44
+ }
45
+ let body = {};
46
+ if (resolvedModel.includes('flux-2/')) {
47
+ body = {
48
+ model: resolvedModel,
49
+ input: {
50
+ prompt,
51
+ aspect_ratio: ratio,
52
+ resolution,
53
+ output_format: 'jpeg', // Force lightweight
54
+ ...(hasReferences ? { input_urls: references } : {}),
55
+ },
56
+ };
57
+ }
58
+ else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
59
+ body = {
60
+ model: resolvedModel,
61
+ input: {
62
+ prompt,
63
+ aspect_ratio: mapRatio(resolvedModel, ratio),
64
+ quality,
65
+ output_format: 'jpeg', // Force lightweight
66
+ ...(hasReferences ? { image_urls: references } : {}),
67
+ },
68
+ };
69
+ }
70
+ else if (resolvedModel === 'nano-banana-pro') {
71
+ body = {
72
+ model: resolvedModel,
73
+ input: {
74
+ prompt,
75
+ aspect_ratio: ratio,
76
+ resolution,
77
+ output_format: 'jpg', // Force lightweight
78
+ ...(hasReferences ? { image_input: references } : {}),
79
+ },
80
+ };
81
+ }
82
+ else if (resolvedModel.includes('google/nano-banana')) {
83
+ body = {
84
+ model: resolvedModel,
85
+ input: {
86
+ prompt,
87
+ image_size: ratio,
88
+ output_format: 'jpeg', // Force lightweight
89
+ ...(hasReferences ? { image_urls: references } : {}),
90
+ },
91
+ };
92
+ }
93
+ else if (resolvedModel === 'midjourney') {
94
+ body = {
95
+ taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
96
+ speed: 'fast',
97
+ prompt,
98
+ fileUrls: references,
99
+ aspectRatio: ratio,
100
+ enableTranslation: true,
101
+ };
102
+ }
103
+ else {
104
+ body = {
105
+ model: 'bytedance/seedream-v4-edit',
106
+ input: {
107
+ image_urls: references,
108
+ prompt,
109
+ image_size: mapRatio('seedream', ratio),
110
+ output_format: 'jpeg',
111
+ },
112
+ };
113
+ }
114
+ return {
115
+ url: resolveGenerationUrl(resolvedModel),
116
+ body,
117
+ requestedModel,
118
+ resolvedModel,
119
+ ratio,
120
+ references,
121
+ hasReferences,
122
+ };
123
+ };
124
+ const extractReferences = (raw, model) => {
125
+ if (!raw || typeof raw !== 'object')
126
+ return [];
127
+ const collection = raw;
128
+ const items = Array.isArray(collection.reference) ? collection.reference : [];
129
+ let limit = 8;
130
+ if (model === 'seedream')
131
+ limit = 14;
132
+ if (model.includes('nano-banana'))
133
+ limit = 10;
134
+ 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);
135
+ };
136
+ class ComandosMedia {
137
+ constructor() {
138
+ this.description = {
139
+ displayName: 'Comandos Media',
140
+ name: 'comandosMedia',
141
+ group: ['transform'],
142
+ version: 1,
143
+ description: 'Create image or video tasks in Comandos API',
144
+ defaults: { name: 'Comandos Media' },
145
+ icon: 'file:Media.png',
146
+ inputs: ['main'],
147
+ outputs: ['main'],
148
+ credentials: [{ name: 'comandosApi', required: true }],
149
+ properties: [
150
+ {
151
+ displayName: 'Operation',
152
+ name: 'operation',
153
+ type: 'options',
154
+ noDataExpression: true,
155
+ options: [
156
+ { name: 'Create Task', value: 'create' },
157
+ { name: 'Check Status', value: 'status' },
158
+ ],
159
+ default: 'create',
160
+ },
161
+ {
162
+ displayName: 'Model',
163
+ name: 'model',
164
+ type: 'options',
165
+ options: [
166
+ { name: 'Seedream 4.5', value: 'seedream' },
167
+ { name: 'Flux Pro', value: 'flux-pro' },
168
+ { name: 'Nano Banana', value: 'nano-banana' },
169
+ { name: 'Nano Banana Pro', value: 'nano-banana-pro' },
170
+ { name: 'Midjourney', value: 'midjourney' },
171
+ ],
172
+ default: 'seedream',
173
+ displayOptions: { show: { operation: ['create'] } },
174
+ },
175
+ {
176
+ displayName: 'Prompt',
177
+ name: 'prompt',
178
+ type: 'string',
179
+ default: '',
180
+ typeOptions: { rows: 4 },
181
+ displayOptions: { show: { operation: ['create'] } },
182
+ },
183
+ {
184
+ displayName: 'Ratio',
185
+ name: 'ratio',
186
+ type: 'options',
187
+ options: [
188
+ { name: '1:1', value: '1:1' },
189
+ { name: '2:3', value: '2:3' },
190
+ { name: '3:2', value: '3:2' },
191
+ { name: '4:5', value: '4:5' },
192
+ { name: '16:9', value: '16:9' },
193
+ { name: '9:16', value: '9:16' },
194
+ { name: '4:3', value: '4:3' },
195
+ { name: '3:4', value: '3:4' },
196
+ { name: '21:9', value: '21:9' },
197
+ ],
198
+ default: '2:3',
199
+ displayOptions: {
200
+ show: { operation: ['create'] },
201
+ },
202
+ },
203
+ {
204
+ displayName: 'Quality',
205
+ name: 'quality',
206
+ type: 'options',
207
+ options: [
208
+ { name: 'Basic (2K)', value: 'basic' },
209
+ { name: 'High (4K)', value: 'high' },
210
+ { name: 'Medium (Balanced)', value: 'medium' },
211
+ ],
212
+ default: 'basic',
213
+ displayOptions: {
214
+ show: { operation: ['create'], model: ['seedream', 'flux-pro'] },
215
+ },
216
+ },
217
+ {
218
+ displayName: 'Resolution',
219
+ name: 'resolution',
220
+ type: 'options',
221
+ options: [
222
+ { name: '1K', value: '1K' },
223
+ { name: '2K', value: '2K' },
224
+ { name: '4K', value: '4K' },
225
+ ],
226
+ default: '1K',
227
+ displayOptions: {
228
+ show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
229
+ },
230
+ },
231
+ {
232
+ displayName: 'References',
233
+ name: 'references',
234
+ type: 'fixedCollection',
235
+ default: {},
236
+ typeOptions: { multipleValues: true },
237
+ options: [
238
+ {
239
+ name: 'reference',
240
+ displayName: 'Reference',
241
+ values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
242
+ },
243
+ ],
244
+ displayOptions: { show: { operation: ['create'] } },
245
+ },
246
+ {
247
+ displayName: 'Task ID',
248
+ name: 'taskId',
249
+ type: 'string',
250
+ default: '',
251
+ displayOptions: { show: { operation: ['status'] } },
252
+ },
253
+ ],
254
+ };
255
+ }
256
+ async execute() {
257
+ const items = this.getInputData();
258
+ const operation = this.getNodeParameter('operation', 0);
259
+ const credentials = await this.getCredentials('comandosApi');
260
+ const licenseKey = String(credentials.licenseKey || '').trim();
261
+ const kieApiKey = String(credentials.kieApiKey || '').trim();
262
+ if (!licenseKey)
263
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'License key is required');
264
+ const results = [];
265
+ for (let i = 0; i < items.length; i += 1) {
266
+ try {
267
+ if (operation === 'create') {
268
+ const model = String(this.getNodeParameter('model', i));
269
+ const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
270
+ const ratio = String(this.getNodeParameter('ratio', i));
271
+ const references = extractReferences(this.getNodeParameter('references', i, {}), model);
272
+ const quality = ['seedream', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
273
+ const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
274
+ if (!prompt)
275
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
276
+ const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution });
277
+ const payload = { ...request, licenseKey, kieApiKey, _v: '0.1.0' };
278
+ const response = await this.helpers.request({
279
+ method: 'POST',
280
+ url: `${COMANDOS_API_URL}/tasks`,
281
+ headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
282
+ body: { process_type: 'IMAGE_GENERATION', payload },
283
+ json: true,
284
+ });
285
+ results.push({ json: response });
286
+ }
287
+ else if (operation === 'status') {
288
+ const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
289
+ if (!taskId)
290
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
291
+ const response = await this.helpers.request({
292
+ method: 'GET',
293
+ url: `${COMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
294
+ headers: { 'X-License-Key': licenseKey },
295
+ json: true,
296
+ });
297
+ results.push({ json: response });
298
+ }
299
+ }
300
+ catch (error) {
301
+ if (error instanceof n8n_workflow_1.NodeOperationError)
302
+ throw error;
303
+ const message = error instanceof Error ? error.message : 'Request failed';
304
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), message, { itemIndex: i });
305
+ }
306
+ }
307
+ return [results];
308
+ }
309
+ }
310
+ exports.ComandosMedia = ComandosMedia;
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ {
2
+ "note": "Example payload template for Commandos Image."
3
+ }
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { ComandosMedia } from './nodes/ComandosMedia.node';
2
+ import { ComandosApi } from './credentials/ComandosApi.credentials';
3
+
4
+ export { ComandosMedia, ComandosApi };
@@ -0,0 +1,334 @@
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 COMANDOS_API_URL = String(process.env.COMANDOS_API_BASE_URL || process.env.COMMANDOS_API_BASE_URL || "https://api.comandos.ai").trim();
11
+ const GENERATION_URL_DEFAULT = String(process.env.COMANDOS_IMAGE_URL_DEFAULT || process.env.COMMANDOS_IMAGE_URL_DEFAULT || "").trim();
12
+ const GENERATION_URL_MJ = String(process.env.COMANDOS_IMAGE_URL_MJ || process.env.COMMANDOS_IMAGE_URL_MJ || "").trim();
13
+
14
+ type BuildParams = {
15
+ model: string;
16
+ prompt: string;
17
+ ratio: string;
18
+ references: string[];
19
+ quality?: string;
20
+ resolution?: string;
21
+ };
22
+
23
+ type BuildResult = {
24
+ url: string;
25
+ body: Record<string, unknown>;
26
+ requestedModel: string;
27
+ resolvedModel: string;
28
+ ratio: string;
29
+ references: string[];
30
+ hasReferences: boolean;
31
+ };
32
+
33
+ const resolveGenerationUrl = (resolvedModel: string): string => {
34
+ if (resolvedModel === 'midjourney') {
35
+ return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
36
+ }
37
+ return GENERATION_URL_DEFAULT;
38
+ };
39
+
40
+ const mapRatio = (model: string, ratio: string): string => {
41
+ const isSeedream = model.includes('seedream');
42
+
43
+ if (isSeedream) {
44
+ if (ratio === '2:3') return 'portrait_3_2';
45
+ if (ratio === '3:2') return 'landscape_2_3';
46
+ }
47
+
48
+ return ratio;
49
+ };
50
+
51
+ const buildGenerationRequest = ({
52
+ model,
53
+ prompt,
54
+ ratio,
55
+ references,
56
+ quality = 'basic',
57
+ resolution = '1K',
58
+ }: BuildParams): BuildResult => {
59
+ const hasReferences = references.length > 0;
60
+ const requestedModel = model;
61
+ let resolvedModel = model;
62
+
63
+ if (requestedModel === 'flux-pro') {
64
+ resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
65
+ } else if (requestedModel === 'nano-banana') {
66
+ resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
67
+ } else if (requestedModel === 'nano-banana-pro') {
68
+ resolvedModel = 'nano-banana-pro';
69
+ } else if (requestedModel === 'seedream') {
70
+ resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
71
+ }
72
+
73
+ if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
74
+ if (requestedModel === 'midjourney') {
75
+ resolvedModel = 'midjourney';
76
+ }
77
+ }
78
+
79
+ let body: Record<string, unknown> = {};
80
+
81
+ if (resolvedModel.includes('flux-2/')) {
82
+ body = {
83
+ model: resolvedModel,
84
+ input: {
85
+ prompt,
86
+ aspect_ratio: ratio,
87
+ resolution,
88
+ output_format: 'jpeg', // Force lightweight
89
+ ...(hasReferences ? { input_urls: references } : {}),
90
+ },
91
+ };
92
+ } else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
93
+ body = {
94
+ model: resolvedModel,
95
+ input: {
96
+ prompt,
97
+ aspect_ratio: mapRatio(resolvedModel, ratio),
98
+ quality,
99
+ output_format: 'jpeg', // Force lightweight
100
+ ...(hasReferences ? { image_urls: references } : {}),
101
+ },
102
+ };
103
+ } else if (resolvedModel === 'nano-banana-pro') {
104
+ body = {
105
+ model: resolvedModel,
106
+ input: {
107
+ prompt,
108
+ aspect_ratio: ratio,
109
+ resolution,
110
+ output_format: 'jpg', // Force lightweight
111
+ ...(hasReferences ? { image_input: references } : {}),
112
+ },
113
+ };
114
+ } else if (resolvedModel.includes('google/nano-banana')) {
115
+ body = {
116
+ model: resolvedModel,
117
+ input: {
118
+ prompt,
119
+ image_size: ratio,
120
+ output_format: 'jpeg', // Force lightweight
121
+ ...(hasReferences ? { image_urls: references } : {}),
122
+ },
123
+ };
124
+ } else if (resolvedModel === 'midjourney') {
125
+ body = {
126
+ taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
127
+ speed: 'fast',
128
+ prompt,
129
+ fileUrls: references,
130
+ aspectRatio: ratio,
131
+ enableTranslation: true,
132
+ };
133
+ } else {
134
+ body = {
135
+ model: 'bytedance/seedream-v4-edit',
136
+ input: {
137
+ image_urls: references,
138
+ prompt,
139
+ image_size: mapRatio('seedream', ratio),
140
+ output_format: 'jpeg',
141
+ },
142
+ };
143
+ }
144
+
145
+ return {
146
+ url: resolveGenerationUrl(resolvedModel),
147
+ body,
148
+ requestedModel,
149
+ resolvedModel,
150
+ ratio,
151
+ references,
152
+ hasReferences,
153
+ };
154
+ };
155
+
156
+ const extractReferences = (raw: unknown, model: string): string[] => {
157
+ if (!raw || typeof raw !== 'object') return [];
158
+ const collection = raw as { reference?: Array<{ url?: string }> };
159
+ const items = Array.isArray(collection.reference) ? collection.reference : [];
160
+ let limit = 8;
161
+ if (model === 'seedream') limit = 14;
162
+ if (model.includes('nano-banana')) limit = 10;
163
+ return items.map((entry) => String(entry?.url || '').trim()).filter((v) => v.length > 0 && /^https?:\/\//i.test(v)).slice(0, limit);
164
+ };
165
+
166
+ export class ComandosMedia implements INodeType {
167
+ description: INodeTypeDescription = {
168
+ displayName: 'Comandos Media',
169
+ name: 'comandosMedia',
170
+ group: ['transform'],
171
+ version: 1,
172
+ description: 'Create image or video tasks in Comandos API',
173
+ defaults: { name: 'Comandos Media' },
174
+ icon: 'file:Media.png',
175
+ inputs: ['main'],
176
+ outputs: ['main'],
177
+ credentials: [{ name: 'comandosApi', required: true }],
178
+ properties: [
179
+ {
180
+ displayName: 'Operation',
181
+ name: 'operation',
182
+ type: 'options',
183
+ noDataExpression: true,
184
+ options: [
185
+ { name: 'Create Task', value: 'create' },
186
+ { name: 'Check Status', value: 'status' },
187
+ ],
188
+ default: 'create',
189
+ },
190
+ {
191
+ displayName: 'Model',
192
+ name: 'model',
193
+ type: 'options',
194
+ options: [
195
+ { name: 'Seedream 4.5', value: 'seedream' },
196
+ { name: 'Flux Pro', value: 'flux-pro' },
197
+ { name: 'Nano Banana', value: 'nano-banana' },
198
+ { name: 'Nano Banana Pro', value: 'nano-banana-pro' },
199
+ { name: 'Midjourney', value: 'midjourney' },
200
+ ],
201
+ default: 'seedream',
202
+ displayOptions: { show: { operation: ['create'] } },
203
+ },
204
+ {
205
+ displayName: 'Prompt',
206
+ name: 'prompt',
207
+ type: 'string',
208
+ default: '',
209
+ typeOptions: { rows: 4 },
210
+ displayOptions: { show: { operation: ['create'] } },
211
+ },
212
+ {
213
+ displayName: 'Ratio',
214
+ name: 'ratio',
215
+ type: 'options',
216
+ options: [
217
+ { name: '1:1', value: '1:1' },
218
+ { name: '2:3', value: '2:3' },
219
+ { name: '3:2', value: '3:2' },
220
+ { name: '4:5', value: '4:5' },
221
+ { name: '16:9', value: '16:9' },
222
+ { name: '9:16', value: '9:16' },
223
+ { name: '4:3', value: '4:3' },
224
+ { name: '3:4', value: '3:4' },
225
+ { name: '21:9', value: '21:9' },
226
+ ],
227
+ default: '2:3',
228
+ displayOptions: {
229
+ show: { operation: ['create'] },
230
+ },
231
+ },
232
+ {
233
+ displayName: 'Quality',
234
+ name: 'quality',
235
+ type: 'options',
236
+ options: [
237
+ { name: 'Basic (2K)', value: 'basic' },
238
+ { name: 'High (4K)', value: 'high' },
239
+ { name: 'Medium (Balanced)', value: 'medium' },
240
+ ],
241
+ default: 'basic',
242
+ displayOptions: {
243
+ show: { operation: ['create'], model: ['seedream', 'flux-pro'] },
244
+ },
245
+ },
246
+ {
247
+ displayName: 'Resolution',
248
+ name: 'resolution',
249
+ type: 'options',
250
+ options: [
251
+ { name: '1K', value: '1K' },
252
+ { name: '2K', value: '2K' },
253
+ { name: '4K', value: '4K' },
254
+ ],
255
+ default: '1K',
256
+ displayOptions: {
257
+ show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
258
+ },
259
+ },
260
+ {
261
+ displayName: 'References',
262
+ name: 'references',
263
+ type: 'fixedCollection',
264
+ default: {},
265
+ typeOptions: { multipleValues: true },
266
+ options: [
267
+ {
268
+ name: 'reference',
269
+ displayName: 'Reference',
270
+ values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
271
+ },
272
+ ],
273
+ displayOptions: { show: { operation: ['create'] } },
274
+ },
275
+ {
276
+ displayName: 'Task ID',
277
+ name: 'taskId',
278
+ type: 'string',
279
+ default: '',
280
+ displayOptions: { show: { operation: ['status'] } },
281
+ },
282
+ ],
283
+ };
284
+
285
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
286
+ const items = this.getInputData();
287
+ const operation = this.getNodeParameter('operation', 0) as string;
288
+ const credentials = await this.getCredentials('comandosApi');
289
+ const licenseKey = String(credentials.licenseKey || '').trim();
290
+ const kieApiKey = String(credentials.kieApiKey || '').trim();
291
+ if (!licenseKey) throw new NodeOperationError(this.getNode(), 'License key is required');
292
+ const results: INodeExecutionData[] = [];
293
+ for (let i = 0; i < items.length; i += 1) {
294
+ try {
295
+ if (operation === 'create') {
296
+ const model = String(this.getNodeParameter('model', i));
297
+ const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
298
+ const ratio = String(this.getNodeParameter('ratio', i));
299
+ const references = extractReferences(this.getNodeParameter('references', i, {}), model);
300
+ const quality = ['seedream', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
301
+ const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
302
+
303
+ if (!prompt) throw new NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
304
+
305
+ const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution });
306
+ const payload = { ...request, licenseKey, kieApiKey, _v: '0.1.0' };
307
+ const response = await this.helpers.request({
308
+ method: 'POST',
309
+ url: `${COMANDOS_API_URL}/tasks`,
310
+ headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
311
+ body: { process_type: 'IMAGE_GENERATION', payload },
312
+ json: true,
313
+ });
314
+ results.push({ json: response as IDataObject });
315
+ } else if (operation === 'status') {
316
+ const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
317
+ if (!taskId) throw new NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
318
+ const response = await this.helpers.request({
319
+ method: 'GET',
320
+ url: `${COMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
321
+ headers: { 'X-License-Key': licenseKey },
322
+ json: true,
323
+ });
324
+ results.push({ json: response as IDataObject });
325
+ }
326
+ } catch (error) {
327
+ if (error instanceof NodeOperationError) throw error;
328
+ const message = error instanceof Error ? error.message : 'Request failed';
329
+ throw new NodeOperationError(this.getNode(), message, { itemIndex: i });
330
+ }
331
+ }
332
+ return [results];
333
+ }
334
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@comandosai/n8n-nodes-media",
3
+ "version": "0.1.2",
4
+ "description": "Comandos Media Generator custom node (Image/Video)",
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 Media.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/ComandosMedia.node.js"
20
+ ],
21
+ "credentials": [
22
+ "dist/credentials/ComandosApi.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
+ }