n8n-nodes-chat2crm 0.1.16 → 0.1.18
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.
|
@@ -121,6 +121,10 @@ class Chat2CrmTrigger {
|
|
|
121
121
|
}
|
|
122
122
|
// Храним последние прочитанные ID для каждого stream
|
|
123
123
|
const lastReadIds = new Map();
|
|
124
|
+
// ИСПРАВЛЕНИЕ: Добавляем флаг для предотвращения параллельных вызовов
|
|
125
|
+
let isReading = false;
|
|
126
|
+
// ИСПРАВЛЕНИЕ: Добавляем Set для отслеживания уже обработанных messageId
|
|
127
|
+
const processedMessageIds = new Set();
|
|
124
128
|
// Собираем информацию о найденных streams для вывода
|
|
125
129
|
const foundStreams = [];
|
|
126
130
|
// Инициализируем lastReadIds для каждого stream и собираем информацию
|
|
@@ -242,87 +246,97 @@ class Chat2CrmTrigger {
|
|
|
242
246
|
let isClosing = false;
|
|
243
247
|
// Функция для чтения сообщений
|
|
244
248
|
const readMessages = async () => {
|
|
245
|
-
//
|
|
246
|
-
if (isClosing) {
|
|
249
|
+
// ИСПРАВЛЕНИЕ: Предотвращаем параллельные вызовы
|
|
250
|
+
if (isReading || isClosing) {
|
|
247
251
|
return;
|
|
248
252
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
const redis = redisConnections.get(db);
|
|
255
|
-
// Читаем каждый stream отдельно через XREAD
|
|
256
|
-
// XREAD читает напрямую из stream, независимо от consumer groups
|
|
257
|
-
for (const stream of dbStreams) {
|
|
253
|
+
isReading = true;
|
|
254
|
+
try {
|
|
255
|
+
for (const [db, dbStreams] of streamsByDb.entries()) {
|
|
258
256
|
if (isClosing) {
|
|
259
257
|
break;
|
|
260
258
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// Используем более короткий block time для возможности быстрого закрытия
|
|
264
|
-
const blockTime = isClosing ? 0 : Math.min(block, MAX_BLOCK_TIME_FOR_RESPONSIVENESS_MS);
|
|
265
|
-
// Используем XREAD чтобы читать все сообщения напрямую из stream
|
|
266
|
-
// Это позволяет читать сообщения даже если они уже обработаны другими consumer'ами
|
|
267
|
-
const messages = await redis.xread('COUNT', count, 'BLOCK', blockTime, 'STREAMS', stream, lastId);
|
|
268
|
-
// Проверяем снова после блокирующей операции
|
|
259
|
+
const redis = redisConnections.get(db);
|
|
260
|
+
for (const stream of dbStreams) {
|
|
269
261
|
if (isClosing) {
|
|
270
|
-
|
|
262
|
+
break;
|
|
271
263
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
264
|
+
try {
|
|
265
|
+
const lastId = lastReadIds.get(stream) || '$';
|
|
266
|
+
const blockTime = isClosing ? 0 : Math.min(block, MAX_BLOCK_TIME_FOR_RESPONSIVENESS_MS);
|
|
267
|
+
const messages = await redis.xread('COUNT', count, 'BLOCK', blockTime, 'STREAMS', stream, lastId);
|
|
268
|
+
if (isClosing) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (messages && Array.isArray(messages) && messages.length > 0) {
|
|
272
|
+
const streamData = messages[0];
|
|
273
|
+
const [streamName, streamMessages] = streamData;
|
|
274
|
+
if (Array.isArray(streamMessages) && streamMessages.length > 0) {
|
|
275
|
+
// ИСПРАВЛЕНИЕ: Фильтруем уже обработанные сообщения
|
|
276
|
+
const newMessages = streamMessages.filter(([messageId]) => {
|
|
277
|
+
const messageKey = `${streamName}:${messageId}`;
|
|
278
|
+
if (processedMessageIds.has(messageKey)) {
|
|
279
|
+
return false; // Пропускаем уже обработанное сообщение
|
|
280
|
+
}
|
|
281
|
+
processedMessageIds.add(messageKey);
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
// ИСПРАВЛЕНИЕ: Обновляем lastReadId сразу после чтения, ДО обработки
|
|
285
|
+
const lastMessageId = streamMessages[streamMessages.length - 1][0];
|
|
286
|
+
lastReadIds.set(streamName, lastMessageId);
|
|
287
|
+
// ИСПРАВЛЕНИЕ: Обрабатываем только новые сообщения
|
|
288
|
+
for (const [messageId, fields] of newMessages) {
|
|
289
|
+
if (isClosing) {
|
|
290
|
+
break;
|
|
293
291
|
}
|
|
294
|
-
|
|
295
|
-
|
|
292
|
+
const messageData = {};
|
|
293
|
+
for (let j = 0; j < fields.length; j += 2) {
|
|
294
|
+
messageData[fields[j]] = fields[j + 1];
|
|
296
295
|
}
|
|
296
|
+
if (messageData.commands && typeof messageData.commands === 'string') {
|
|
297
|
+
try {
|
|
298
|
+
messageData.commands = JSON.parse(messageData.commands);
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
// Ignore parse errors
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const executionData = {
|
|
305
|
+
json: {
|
|
306
|
+
messageId,
|
|
307
|
+
stream: streamName,
|
|
308
|
+
data: messageData,
|
|
309
|
+
commandList: messageData,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
this.emit([[executionData]]);
|
|
313
|
+
}
|
|
314
|
+
// ИСПРАВЛЕНИЕ: Очищаем старые записи из Set для предотвращения утечки памяти
|
|
315
|
+
// Оставляем только последние 1000 обработанных ID
|
|
316
|
+
if (processedMessageIds.size > 1000) {
|
|
317
|
+
const entries = Array.from(processedMessageIds);
|
|
318
|
+
const toRemove = entries.slice(0, entries.length - 1000);
|
|
319
|
+
toRemove.forEach(id => processedMessageIds.delete(id));
|
|
297
320
|
}
|
|
298
|
-
// Триггерим workflow с данными сообщения
|
|
299
|
-
const executionData = {
|
|
300
|
-
json: {
|
|
301
|
-
messageId,
|
|
302
|
-
stream: streamName,
|
|
303
|
-
data: messageData,
|
|
304
|
-
commandList: messageData,
|
|
305
|
-
},
|
|
306
|
-
};
|
|
307
|
-
this.emit([[executionData]]);
|
|
308
321
|
}
|
|
309
322
|
}
|
|
310
323
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
continue;
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (isClosing && (error.message?.includes('Connection is closed') || error.message?.includes('disconnect'))) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (error.message?.includes('Invalid stream ID') || error.message?.includes('no such key')) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
console.error(`[Chat2Crm Trigger] Error reading stream ${stream} from Redis DB ${db}:`, error);
|
|
320
332
|
}
|
|
321
|
-
// Логируем только критические ошибки
|
|
322
|
-
console.error(`[Chat2Crm Trigger] Error reading stream ${stream} from Redis DB ${db}:`, error);
|
|
323
333
|
}
|
|
324
334
|
}
|
|
325
335
|
}
|
|
336
|
+
finally {
|
|
337
|
+
// ИСПРАВЛЕНИЕ: Всегда сбрасываем флаг, даже при ошибке
|
|
338
|
+
isReading = false;
|
|
339
|
+
}
|
|
326
340
|
};
|
|
327
341
|
// Начинаем polling с более коротким интервалом
|
|
328
342
|
let interval = null;
|
|
@@ -80,6 +80,12 @@ exports.STREAM_DEFINITIONS = [
|
|
|
80
80
|
description: 'Outgoing messages from N8N (DB 1)',
|
|
81
81
|
db: 1,
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: 'N8N Chat Control',
|
|
85
|
+
value: 'N8N_CHAT_CONTROL',
|
|
86
|
+
description: 'Chat control messages for N8N (DB 1)',
|
|
87
|
+
db: 1,
|
|
88
|
+
},
|
|
83
89
|
];
|
|
84
90
|
/**
|
|
85
91
|
* Маппинг stream names к номерам баз данных
|
|
@@ -92,6 +98,7 @@ exports.STREAM_DB_MAP = {
|
|
|
92
98
|
crm_outgoing_status: 1,
|
|
93
99
|
N8N_INCOMING_MESSAGE: 1,
|
|
94
100
|
N8N_OUTGOING_MESSAGE: 1,
|
|
101
|
+
N8N_CHAT_CONTROL: 1,
|
|
95
102
|
// Поддержка старого формата (строчными) для обратной совместимости
|
|
96
103
|
n8n_incoming_message: 1,
|
|
97
104
|
n8n_outgoing_message: 1,
|
package/index.js
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
+
// ========================================
|
|
2
|
+
// PROXY CONFIGURATION - ЛОГИРОВАНИЕ ПРОКСИ
|
|
3
|
+
// ========================================
|
|
4
|
+
const proxyVars = {
|
|
5
|
+
'all_proxy': process.env.all_proxy,
|
|
6
|
+
'ALL_PROXY': process.env.ALL_PROXY,
|
|
7
|
+
'http_proxy': process.env.http_proxy,
|
|
8
|
+
'HTTP_PROXY': process.env.HTTP_PROXY,
|
|
9
|
+
'https_proxy': process.env.https_proxy,
|
|
10
|
+
'HTTPS_PROXY': process.env.HTTPS_PROXY,
|
|
11
|
+
'no_proxy': process.env.no_proxy,
|
|
12
|
+
'NO_PROXY': process.env.NO_PROXY,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Находим активный прокси (lowercase имеет приоритет)
|
|
16
|
+
const activeProxy = process.env.all_proxy || process.env.ALL_PROXY ||
|
|
17
|
+
process.env.http_proxy || process.env.HTTP_PROXY ||
|
|
18
|
+
process.env.https_proxy || process.env.HTTPS_PROXY;
|
|
19
|
+
|
|
20
|
+
// Выводим информацию о прокси БОЛЬШИМИ БУКВАМИ, чтобы было видно
|
|
21
|
+
console.log('\n');
|
|
22
|
+
console.log('╔════════════════════════════════════════════════════════════════╗');
|
|
23
|
+
console.log('║ PROXY CONFIGURATION ║');
|
|
24
|
+
console.log('╚════════════════════════════════════════════════════════════════╝');
|
|
25
|
+
console.log('');
|
|
26
|
+
|
|
27
|
+
if (activeProxy) {
|
|
28
|
+
// Маскируем пароль в логах
|
|
29
|
+
const maskedProxy = activeProxy.replace(/:\/\/[^:]+:[^@]+@/, '://***:***@');
|
|
30
|
+
|
|
31
|
+
console.log('✅ PROXY IS CONFIGURED AND ACTIVE');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log('Active Proxy URL:', maskedProxy);
|
|
34
|
+
console.log('NO_PROXY:', process.env.no_proxy || process.env.NO_PROXY || 'not set');
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log('All proxy environment variables:');
|
|
37
|
+
Object.entries(proxyVars).forEach(([key, value]) => {
|
|
38
|
+
if (value) {
|
|
39
|
+
const masked = key.toLowerCase().includes('proxy') && !key.toLowerCase().includes('no')
|
|
40
|
+
? value.replace(/:\/\/[^:]+:[^@]+@/, '://***:***@')
|
|
41
|
+
: value;
|
|
42
|
+
console.log(` ${key.padEnd(15)} = ${masked}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('⚠️ All HTTP/HTTPS requests from n8n nodes will use this proxy');
|
|
47
|
+
console.log('⚠️ Redis connections (TCP) do NOT use HTTP proxy');
|
|
48
|
+
} else {
|
|
49
|
+
console.log('❌ NO PROXY CONFIGURED');
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log('Available proxy environment variables (all empty):');
|
|
52
|
+
Object.keys(proxyVars).forEach(key => {
|
|
53
|
+
console.log(` ${key.padEnd(15)} = ${proxyVars[key] || '(not set)'}`);
|
|
54
|
+
});
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log('To enable proxy, set in .env file:');
|
|
57
|
+
console.log(' ALL_PROXY=http://user:pass@host:port');
|
|
58
|
+
console.log(' all_proxy=http://user:pass@host:port');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log('╔════════════════════════════════════════════════════════════════╗');
|
|
63
|
+
console.log('║ END OF PROXY CONFIGURATION ║');
|
|
64
|
+
console.log('╚════════════════════════════════════════════════════════════════╝');
|
|
65
|
+
console.log('\n');
|
|
66
|
+
|
|
1
67
|
module.exports = {
|
|
2
68
|
nodes: [
|
|
3
69
|
require('./dist/nodes/Chat2CrmTrigger/Chat2CrmTrigger.node.js'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-chat2crm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "n8n node for Chat2Crm Redis integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"main": "index.js",
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "rm -rf dist && tsc && gulp build:icons",
|
|
14
|
-
"dev": "npm run build &&
|
|
14
|
+
"dev": "npm run build && node -r dotenv/config -r ./proxy-logger.js node_modules/.bin/n8n",
|
|
15
15
|
"dev:watch": "tsc --watch",
|
|
16
16
|
"format": "prettier nodes credentials --write",
|
|
17
17
|
"lint": "eslint \"nodes/**/*.ts\" \"credentials/**/*.ts\" \"package.json\"",
|