@comandosai/n8n-nodes-rss-bridge 0.1.1

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/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Commandos RSS (кастомная нода n8n)
2
+
3
+ Нода выполняет два последовательных запроса в Commandos API: `detect`, затем `collect`.
4
+ Оба запроса должны работать синхронно через вебхук `comandos_rss`.
5
+
6
+ ## Где находится
7
+
8
+ - Код ноды: `/root/sandbox/nodes/rss-bridge`
9
+ - Пакет: `@comandosai/n8n-nodes-rss-bridge`
10
+
11
+ ## Требования
12
+
13
+ - n8n (dev окружение)
14
+ - Node.js 18+
15
+ - Доступ к Commandos API (`https://api.comandos.ai`)
16
+ - `RSS_BRIDGE` включён в `SYNC_PROCESS_TYPES` на API (иначе API вернёт `taskId` вместо ответа).
17
+
18
+ ## Доступ
19
+
20
+ Нода работает только через Commandos API и требует лицензионный ключ.
21
+ Без ключа запросы не будут выполнены.
22
+
23
+ ## Подключение в n8n-dev
24
+
25
+ В `docker-compose` уже настроено:
26
+
27
+ - монтирование `/root/sandbox/nodes` в `/custom/commandos`
28
+ - переменная `N8N_CUSTOM_EXTENSIONS=/custom/commandos`
29
+
30
+ После изменений перезапусти n8n.
31
+
32
+ ## Credentials
33
+
34
+ Тип: `Commandos API`
35
+
36
+ Поля:
37
+ - `License Key` — передаётся как `X-License-Key` в API
38
+
39
+ ## Параметры
40
+
41
+ - `URL список` — строка с URL, разделители: перенос строки, запятая, точка с запятой
42
+ - `Период` — `Часы` или `Дни`
43
+ - `Значение периода (0-100)` — 0 отключает фильтрацию
44
+ - `Точка отсчета (ISO, опционально)` — если не задано, используется текущее время сервера
45
+
46
+ ## Выход
47
+
48
+ ```json
49
+ {
50
+ "detect": { ... },
51
+ "collect": { ... }
52
+ }
53
+ ```
54
+
55
+ ## Сборка
56
+
57
+ ```bash
58
+ npm install
59
+ npm run build
60
+ ```
61
+
62
+ После сборки перезапусти n8n, чтобы нода появилась в списке.
package/RSS.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
2
+ <rect width="128" height="128" rx="16" fill="#ff8a00" />
3
+ <circle cx="32" cy="96" r="10" fill="#ffffff" />
4
+ <path d="M24 68a36 36 0 0 1 36 36" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" />
5
+ <path d="M24 44a60 60 0 0 1 60 60" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" />
6
+ </svg>
package/RSSS.png ADDED
Binary file
@@ -0,0 +1,19 @@
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
+ }
@@ -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,23 @@
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
+ }
22
+ }
23
+ exports.ComandosApi = ComandosApi;
@@ -0,0 +1,3 @@
1
+ import { ComandosRssBridge } from './nodes/ComandosRssBridge.node';
2
+ import { ComandosApi } from './credentials/ComandosApi.credentials';
3
+ export { ComandosRssBridge, ComandosApi };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComandosApi = exports.ComandosRssBridge = void 0;
4
+ const ComandosRssBridge_node_1 = require("./nodes/ComandosRssBridge.node");
5
+ Object.defineProperty(exports, "ComandosRssBridge", { enumerable: true, get: function () { return ComandosRssBridge_node_1.ComandosRssBridge; } });
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,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class ComandosRssBridge implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComandosRssBridge = 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 splitUrls = (raw) => raw
7
+ .split(/[\n,;]+/)
8
+ .map((entry) => entry.trim())
9
+ .filter((entry) => entry.length > 0);
10
+ const normalizeUrls = (value) => {
11
+ if (Array.isArray(value)) {
12
+ return value.map((entry) => String(entry || '').trim()).filter((entry) => entry.length > 0);
13
+ }
14
+ if (typeof value === 'string') {
15
+ return splitUrls(value);
16
+ }
17
+ return [];
18
+ };
19
+ const normalizeSinceValue = (value, itemIndex, node) => {
20
+ if (typeof value === 'number' && Number.isFinite(value)) {
21
+ return value;
22
+ }
23
+ const raw = String(value || '').trim();
24
+ if (raw === '') {
25
+ return 0;
26
+ }
27
+ if (!/^\d+$/.test(raw)) {
28
+ throw new n8n_workflow_1.NodeOperationError(node.getNode(), 'Значение периода должно быть числом', {
29
+ itemIndex,
30
+ });
31
+ }
32
+ return Number(raw);
33
+ };
34
+ class ComandosRssBridge {
35
+ constructor() {
36
+ this.description = {
37
+ displayName: 'Comandos RSS',
38
+ name: 'comandosRssBridge',
39
+ group: ['transform'],
40
+ version: 1,
41
+ description: 'Последовательные запросы detect + collect через Comandos API',
42
+ defaults: {
43
+ name: 'Comandos RSS',
44
+ },
45
+ icon: 'file:RSSS.png',
46
+ inputs: ['main'],
47
+ outputs: ['main'],
48
+ credentials: [
49
+ {
50
+ name: 'comandosApi',
51
+ required: true,
52
+ },
53
+ ],
54
+ properties: [
55
+ {
56
+ displayName: 'URL список',
57
+ name: 'urls',
58
+ type: 'string',
59
+ default: '',
60
+ placeholder: 'https://t.me/ai_comandos\nhttps://www.youtube.com/@kokorevinvest',
61
+ typeOptions: {
62
+ rows: 6,
63
+ },
64
+ description: 'URL можно разделять переносом строки, запятой или точкой с запятой',
65
+ },
66
+ {
67
+ displayName: 'Период',
68
+ name: 'sinceUnit',
69
+ type: 'options',
70
+ options: [
71
+ { name: 'Часы', value: 'hours' },
72
+ { name: 'Дни', value: 'days' },
73
+ ],
74
+ default: 'days',
75
+ },
76
+ {
77
+ displayName: 'Значение периода (0-100)',
78
+ name: 'sinceValue',
79
+ type: 'number',
80
+ default: 5,
81
+ },
82
+ {
83
+ displayName: 'Точка отсчета (ISO, опционально)',
84
+ name: 'now',
85
+ type: 'string',
86
+ default: '',
87
+ placeholder: '2026-01-03T12:00:00+00:00',
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ async execute() {
93
+ const items = this.getInputData();
94
+ const credentials = await this.getCredentials('comandosApi');
95
+ const licenseKey = String(credentials.licenseKey || '').trim();
96
+ if (!licenseKey) {
97
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Лицензионный ключ обязателен');
98
+ }
99
+ const results = [];
100
+ for (let i = 0; i < items.length; i += 1) {
101
+ const rawUrls = this.getNodeParameter('urls', i);
102
+ const sinceUnit = String(this.getNodeParameter('sinceUnit', i) || 'days');
103
+ const sinceValue = normalizeSinceValue(this.getNodeParameter('sinceValue', i), i, this);
104
+ const now = String(this.getNodeParameter('now', i) || '').trim();
105
+ const urls = normalizeUrls(rawUrls);
106
+ if (urls.length === 0) {
107
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Нужно указать хотя бы один URL', {
108
+ itemIndex: i,
109
+ });
110
+ }
111
+ if (sinceValue < 0 || sinceValue > 100) {
112
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Значение периода должно быть в диапазоне 0-100', {
113
+ itemIndex: i,
114
+ });
115
+ }
116
+ const detectResponse = await this.helpers.request({
117
+ method: 'POST',
118
+ url: `${COMANDOS_API_URL}/tasks`,
119
+ json: true,
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ 'X-License-Key': licenseKey,
123
+ },
124
+ body: {
125
+ process_type: 'RSS_BRIDGE',
126
+ payload: {
127
+ action: 'detect',
128
+ urls,
129
+ },
130
+ },
131
+ });
132
+ if (detectResponse && detectResponse.taskId) {
133
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'RSS_BRIDGE должен работать синхронно. Добавьте RSS_BRIDGE в SYNC_PROCESS_TYPES на API.', { itemIndex: i });
134
+ }
135
+ const detections = Array.isArray(detectResponse === null || detectResponse === void 0 ? void 0 : detectResponse.detections)
136
+ ? detectResponse.detections
137
+ : [];
138
+ const collectPayload = {
139
+ action: 'collect',
140
+ urls,
141
+ detections,
142
+ since_value: sinceValue,
143
+ since_unit: sinceUnit,
144
+ };
145
+ if (now) {
146
+ collectPayload.now = now;
147
+ }
148
+ const collectResponse = await this.helpers.request({
149
+ method: 'POST',
150
+ url: `${COMANDOS_API_URL}/tasks`,
151
+ json: true,
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ 'X-License-Key': licenseKey,
155
+ },
156
+ body: {
157
+ process_type: 'RSS_BRIDGE',
158
+ payload: collectPayload,
159
+ },
160
+ });
161
+ results.push({
162
+ json: {
163
+ detect: detectResponse,
164
+ collect: collectResponse,
165
+ },
166
+ });
167
+ }
168
+ return [results];
169
+ }
170
+ }
171
+ exports.ComandosRssBridge = ComandosRssBridge;
Binary file
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { ComandosRssBridge } from './nodes/ComandosRssBridge.node';
2
+ import { ComandosApi } from './credentials/ComandosApi.credentials';
3
+
4
+ export { ComandosRssBridge, ComandosApi };
@@ -0,0 +1,195 @@
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
+
12
+ const splitUrls = (raw: string): string[] =>
13
+ raw
14
+ .split(/[\n,;]+/)
15
+ .map((entry) => entry.trim())
16
+ .filter((entry) => entry.length > 0);
17
+
18
+ const normalizeUrls = (value: unknown): string[] => {
19
+ if (Array.isArray(value)) {
20
+ return value.map((entry) => String(entry || '').trim()).filter((entry) => entry.length > 0);
21
+ }
22
+ if (typeof value === 'string') {
23
+ return splitUrls(value);
24
+ }
25
+ return [];
26
+ };
27
+
28
+ const normalizeSinceValue = (value: unknown, itemIndex: number, node: IExecuteFunctions): number => {
29
+ if (typeof value === 'number' && Number.isFinite(value)) {
30
+ return value;
31
+ }
32
+ const raw = String(value || '').trim();
33
+ if (raw === '') {
34
+ return 0;
35
+ }
36
+ if (!/^\d+$/.test(raw)) {
37
+ throw new NodeOperationError(node.getNode(), 'Значение периода должно быть числом', {
38
+ itemIndex,
39
+ });
40
+ }
41
+ return Number(raw);
42
+ };
43
+
44
+ export class ComandosRssBridge implements INodeType {
45
+ description: INodeTypeDescription = {
46
+ displayName: 'Comandos RSS',
47
+ name: 'comandosRssBridge',
48
+ group: ['transform'],
49
+ version: 1,
50
+ description: 'Последовательные запросы detect + collect через Comandos API',
51
+ defaults: {
52
+ name: 'Comandos RSS',
53
+ },
54
+ icon: 'file:RSSS.png',
55
+ inputs: ['main'],
56
+ outputs: ['main'],
57
+ credentials: [
58
+ {
59
+ name: 'comandosApi',
60
+ required: true,
61
+ },
62
+ ],
63
+ properties: [
64
+ {
65
+ displayName: 'URL список',
66
+ name: 'urls',
67
+ type: 'string',
68
+ default: '',
69
+ placeholder: 'https://t.me/ai_comandos\nhttps://www.youtube.com/@kokorevinvest',
70
+ typeOptions: {
71
+ rows: 6,
72
+ },
73
+ description: 'URL можно разделять переносом строки, запятой или точкой с запятой',
74
+ },
75
+ {
76
+ displayName: 'Период',
77
+ name: 'sinceUnit',
78
+ type: 'options',
79
+ options: [
80
+ { name: 'Часы', value: 'hours' },
81
+ { name: 'Дни', value: 'days' },
82
+ ],
83
+ default: 'days',
84
+ },
85
+ {
86
+ displayName: 'Значение периода (0-100)',
87
+ name: 'sinceValue',
88
+ type: 'number',
89
+ default: 5,
90
+ },
91
+ {
92
+ displayName: 'Точка отсчета (ISO, опционально)',
93
+ name: 'now',
94
+ type: 'string',
95
+ default: '',
96
+ placeholder: '2026-01-03T12:00:00+00:00',
97
+ },
98
+ ],
99
+ };
100
+
101
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
102
+ const items = this.getInputData();
103
+ const credentials = await this.getCredentials('comandosApi');
104
+ const licenseKey = String(credentials.licenseKey || '').trim();
105
+
106
+ if (!licenseKey) {
107
+ throw new NodeOperationError(this.getNode(), 'Лицензионный ключ обязателен');
108
+ }
109
+
110
+ const results: INodeExecutionData[] = [];
111
+
112
+ for (let i = 0; i < items.length; i += 1) {
113
+ const rawUrls = this.getNodeParameter('urls', i);
114
+ const sinceUnit = String(this.getNodeParameter('sinceUnit', i) || 'days');
115
+ const sinceValue = normalizeSinceValue(this.getNodeParameter('sinceValue', i), i, this);
116
+ const now = String(this.getNodeParameter('now', i) || '').trim();
117
+ const urls = normalizeUrls(rawUrls);
118
+
119
+ if (urls.length === 0) {
120
+ throw new NodeOperationError(this.getNode(), 'Нужно указать хотя бы один URL', {
121
+ itemIndex: i,
122
+ });
123
+ }
124
+ if (sinceValue < 0 || sinceValue > 100) {
125
+ throw new NodeOperationError(this.getNode(), 'Значение периода должно быть в диапазоне 0-100', {
126
+ itemIndex: i,
127
+ });
128
+ }
129
+
130
+ const detectResponse = await this.helpers.request({
131
+ method: 'POST',
132
+ url: `${COMANDOS_API_URL}/tasks`,
133
+ json: true,
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'X-License-Key': licenseKey,
137
+ },
138
+ body: {
139
+ process_type: 'RSS_BRIDGE',
140
+ payload: {
141
+ action: 'detect',
142
+ urls,
143
+ },
144
+ },
145
+ });
146
+
147
+ if (detectResponse && (detectResponse as IDataObject).taskId) {
148
+ throw new NodeOperationError(
149
+ this.getNode(),
150
+ 'RSS_BRIDGE должен работать синхронно. Добавьте RSS_BRIDGE в SYNC_PROCESS_TYPES на API.',
151
+ { itemIndex: i },
152
+ );
153
+ }
154
+
155
+ const detections = Array.isArray((detectResponse as IDataObject)?.detections)
156
+ ? ((detectResponse as IDataObject).detections as IDataObject[])
157
+ : [];
158
+
159
+ const collectPayload: IDataObject = {
160
+ action: 'collect',
161
+ urls,
162
+ detections,
163
+ since_value: sinceValue,
164
+ since_unit: sinceUnit,
165
+ };
166
+
167
+ if (now) {
168
+ collectPayload.now = now;
169
+ }
170
+
171
+ const collectResponse = await this.helpers.request({
172
+ method: 'POST',
173
+ url: `${COMANDOS_API_URL}/tasks`,
174
+ json: true,
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ 'X-License-Key': licenseKey,
178
+ },
179
+ body: {
180
+ process_type: 'RSS_BRIDGE',
181
+ payload: collectPayload,
182
+ },
183
+ });
184
+
185
+ results.push({
186
+ json: {
187
+ detect: detectResponse as IDataObject,
188
+ collect: collectResponse as IDataObject,
189
+ },
190
+ });
191
+ }
192
+
193
+ return [results];
194
+ }
195
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@comandosai/n8n-nodes-rss-bridge",
3
+ "version": "0.1.1",
4
+ "description": "Comandos RSS Bridge custom node",
5
+ "author": "Comandos AI",
6
+ "license": "UNLICENSED",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json && mkdir -p dist/nodes && cp -f RSSS.png dist/nodes/"
11
+ },
12
+ "dependencies": {
13
+ "n8n-workflow": "^1.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^18.19.0",
17
+ "typescript": "^5.4.5"
18
+ },
19
+ "n8n": {
20
+ "nodes": [
21
+ "dist/nodes/ComandosRssBridge.node.js"
22
+ ],
23
+ "credentials": [
24
+ "dist/credentials/ComandosApi.credentials.js"
25
+ ]
26
+ }
27
+ }
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
+ }