node-red-contrib-knx-ultimate 6.3.28 → 6.3.30
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/CHANGELOG.md +5 -1
- package/nodes/knxUltimateAI.js +6 -3
- package/nodes/locales/de/knxUltimateAI.html +1 -1
- package/nodes/locales/en/knxUltimateAI.html +1 -1
- package/nodes/locales/es/knxUltimateAI.html +1 -1
- package/nodes/locales/fr/knxUltimateAI.html +1 -1
- package/nodes/locales/it/knxUltimateAI.html +1 -1
- package/nodes/locales/zh-CN/knxUltimateAI.html +1 -1
- package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -1
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +4 -4
- package/nodes/utils/knxAiTelegramVoice.js +74 -4
- package/package.json +1 -1
- package/resources/KNXAIChatAdapterMappings.js +60 -5
|
@@ -128,15 +128,47 @@ const resolveTelegramVoiceAllowedOrigin = (message) => {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
const applyKnxAiTelegramVoiceInputPresetFallback = ({ preset, message } = {}) => {
|
|
131
|
-
|
|
131
|
+
const normalizedPreset = String(preset || '')
|
|
132
|
+
if (!['windkh-telegrambot', 'redbot-telegram'].includes(normalizedPreset) || !message || typeof message !== 'object') return null
|
|
132
133
|
const telegram = message.payload && typeof message.payload === 'object' ? message.payload : null
|
|
133
|
-
if (!telegram
|
|
134
|
+
if (!telegram) return null
|
|
134
135
|
const chatId = telegram.chatId
|
|
135
136
|
if (chatId === undefined || chatId === null || chatId === '') return null
|
|
136
137
|
const original = message.originalMessage && typeof message.originalMessage === 'object'
|
|
137
138
|
? message.originalMessage
|
|
138
139
|
: {}
|
|
139
140
|
const voice = findTelegramVoiceMetadata(message)
|
|
141
|
+
|
|
142
|
+
if (normalizedPreset === 'redbot-telegram') {
|
|
143
|
+
const redbotType = String(telegram.type || '').trim().toLowerCase()
|
|
144
|
+
if ((redbotType !== 'audio' && redbotType !== 'voice') || !Buffer.isBuffer(telegram.content)) return null
|
|
145
|
+
const mediaType = normalizeVoiceMediaType(voice.mime_type || telegram.mimeType)
|
|
146
|
+
message.sessionId = String(chatId)
|
|
147
|
+
message.language = (original.from && original.from.language_code) ||
|
|
148
|
+
(original.message && original.message.from && original.message.from.language_code) ||
|
|
149
|
+
telegram.language ||
|
|
150
|
+
message.language ||
|
|
151
|
+
''
|
|
152
|
+
message.topic = 'ask'
|
|
153
|
+
delete message.prompt
|
|
154
|
+
message.knxAi = Object.assign({}, message.knxAi, {
|
|
155
|
+
sessionId: String(chatId),
|
|
156
|
+
voiceInput: {
|
|
157
|
+
source: 'telegram',
|
|
158
|
+
originalType: 'voice',
|
|
159
|
+
transport: 'redbot-buffer',
|
|
160
|
+
data: telegram.content,
|
|
161
|
+
fileId: String(voice.file_id || '').trim(),
|
|
162
|
+
mediaType,
|
|
163
|
+
filename: sanitizeVoiceFilename({ filename: telegram.filename || voice.file_name, mediaType }),
|
|
164
|
+
durationSeconds: Math.max(0, Number(voice.duration || telegram.duration) || 0),
|
|
165
|
+
fileSize: Math.max(0, Number(voice.file_size || telegram.fileSize) || telegram.content.length)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
return message
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (telegram.type !== 'voice') return null
|
|
140
172
|
const mediaType = normalizeVoiceMediaType(voice.mime_type || telegram.contentType || telegram.mimeType)
|
|
141
173
|
const fileId = String(telegram.content || voice.file_id || '').trim()
|
|
142
174
|
const weblink = String(telegram.weblink || message.weblink || '').trim()
|
|
@@ -175,6 +207,7 @@ const redactKnxAiTelegramVoiceLocations = (message) => {
|
|
|
175
207
|
if (!message || typeof message !== 'object') return message
|
|
176
208
|
if (message.payload && typeof message.payload === 'object') {
|
|
177
209
|
message.payload = Object.assign({}, message.payload)
|
|
210
|
+
if (Buffer.isBuffer(message.payload.content)) message.payload.content = ''
|
|
178
211
|
delete message.payload.weblink
|
|
179
212
|
delete message.payload.path
|
|
180
213
|
}
|
|
@@ -182,6 +215,7 @@ const redactKnxAiTelegramVoiceLocations = (message) => {
|
|
|
182
215
|
delete message.path
|
|
183
216
|
if (message.knxAi && message.knxAi.voiceInput && typeof message.knxAi.voiceInput === 'object') {
|
|
184
217
|
const voiceInput = Object.assign({}, message.knxAi.voiceInput)
|
|
218
|
+
delete voiceInput.data
|
|
185
219
|
delete voiceInput.weblink
|
|
186
220
|
delete voiceInput.path
|
|
187
221
|
message.knxAi = Object.assign({}, message.knxAi, { voiceInput })
|
|
@@ -258,6 +292,16 @@ const fetchKnxAiTelegramVoice = async ({
|
|
|
258
292
|
}
|
|
259
293
|
const mediaType = normalizeVoiceMediaType(source.mediaType)
|
|
260
294
|
const filename = sanitizeVoiceFilename({ filename: source.filename, mediaType })
|
|
295
|
+
if (Buffer.isBuffer(source.data)) {
|
|
296
|
+
if (!source.data.length) throw new Error('Telegram returned an empty voice message')
|
|
297
|
+
if (source.data.length > maxBytes) throw new Error(`Voice audio exceeds the ${Math.round(maxBytes / (1024 * 1024))} MB limit`)
|
|
298
|
+
return {
|
|
299
|
+
data: Buffer.from(source.data),
|
|
300
|
+
mediaType,
|
|
301
|
+
filename,
|
|
302
|
+
source: String(source.transport || 'telegram-buffer')
|
|
303
|
+
}
|
|
304
|
+
}
|
|
261
305
|
const weblink = String(source.weblink || '').trim()
|
|
262
306
|
if (weblink) {
|
|
263
307
|
let parsed
|
|
@@ -409,11 +453,15 @@ const postKnxAiVoiceSpeech = async ({
|
|
|
409
453
|
}
|
|
410
454
|
|
|
411
455
|
const applyKnxAiTelegramVoiceOutputPresetFallback = ({ preset, message, inputMessage } = {}) => {
|
|
412
|
-
|
|
456
|
+
const normalizedPreset = String(preset || '')
|
|
457
|
+
if (!['windkh-telegrambot', 'redbot-telegram'].includes(normalizedPreset) || !message || typeof message !== 'object') return message
|
|
413
458
|
const audio = message.knxAi && message.knxAi.audio
|
|
414
459
|
if (!audio || !Buffer.isBuffer(audio.data) || !audio.data.length) return message
|
|
415
460
|
const currentPayload = message.payload && typeof message.payload === 'object' ? message.payload : null
|
|
416
|
-
if (currentPayload && (currentPayload.type === 'voice' || currentPayload.type === 'photo')) return message
|
|
461
|
+
if (currentPayload && (currentPayload.type === 'voice' || currentPayload.type === 'audio' || currentPayload.type === 'photo')) return message
|
|
462
|
+
if (normalizedPreset === 'redbot-telegram' && currentPayload && currentPayload.type === 'inline-buttons') return message
|
|
463
|
+
const confirmation = message.knxAi && message.knxAi.confirmationRequest
|
|
464
|
+
if (normalizedPreset === 'redbot-telegram' && confirmation && confirmation.required === true) return message
|
|
417
465
|
const source = inputMessage && typeof inputMessage === 'object'
|
|
418
466
|
? inputMessage
|
|
419
467
|
: message.inputMessage && typeof message.inputMessage === 'object'
|
|
@@ -435,6 +483,28 @@ const applyKnxAiTelegramVoiceOutputPresetFallback = ({ preset, message, inputMes
|
|
|
435
483
|
.filter(Boolean)
|
|
436
484
|
.join('\n')
|
|
437
485
|
.slice(0, 1024)
|
|
486
|
+
|
|
487
|
+
if (normalizedPreset === 'redbot-telegram') {
|
|
488
|
+
const transport = currentPayload && currentPayload.transport
|
|
489
|
+
? currentPayload.transport
|
|
490
|
+
: sourcePayload.transport || 'telegram'
|
|
491
|
+
const userId = currentPayload && currentPayload.userId !== undefined
|
|
492
|
+
? currentPayload.userId
|
|
493
|
+
: sourcePayload.userId
|
|
494
|
+
message.payload = {
|
|
495
|
+
transport,
|
|
496
|
+
chatId,
|
|
497
|
+
type: 'audio',
|
|
498
|
+
inbound: false,
|
|
499
|
+
content: audio.data,
|
|
500
|
+
filename: sanitizeVoiceFilename({ filename: audio.filename, mediaType: audio.mediaType, fallback: 'knx-ai-reply' }),
|
|
501
|
+
mimeType: normalizeVoiceMediaType(audio.mediaType),
|
|
502
|
+
caption
|
|
503
|
+
}
|
|
504
|
+
if (userId !== undefined) message.payload.userId = userId
|
|
505
|
+
return message
|
|
506
|
+
}
|
|
507
|
+
|
|
438
508
|
const options = Object.assign({}, currentPayload && currentPayload.options ? currentPayload.options : {})
|
|
439
509
|
if (caption) options.caption = caption
|
|
440
510
|
message.payload = {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "6.3.
|
|
6
|
+
"version": "6.3.30",
|
|
7
7
|
"description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT and Modbus adapters, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -193,15 +193,13 @@ return msg;`
|
|
|
193
193
|
id: 'redbot-telegram',
|
|
194
194
|
title: 'RedBot / node-red-contrib-chatbot (Telegram)',
|
|
195
195
|
inputCode: `// RedBot Telegram Receiver -> KNX AI
|
|
196
|
-
// RedBot normalizes Telegram text and inline-button postbacks in msg.payload.
|
|
196
|
+
// RedBot normalizes Telegram text, voice audio and inline-button postbacks in msg.payload.
|
|
197
197
|
const redbot = msg.payload;
|
|
198
198
|
if (!redbot || typeof redbot !== 'object') return;
|
|
199
199
|
if (redbot.transport && redbot.transport !== 'telegram') return;
|
|
200
200
|
|
|
201
201
|
const chatId = redbot.chatId;
|
|
202
|
-
|
|
203
|
-
if (chatId === undefined || chatId === null || content === '') return;
|
|
204
|
-
if (redbot.type !== 'message') return;
|
|
202
|
+
if (chatId === undefined || chatId === null) return;
|
|
205
203
|
|
|
206
204
|
msg.sessionId = String(chatId);
|
|
207
205
|
msg.language =
|
|
@@ -210,6 +208,37 @@ msg.language =
|
|
|
210
208
|
msg.language ||
|
|
211
209
|
'';
|
|
212
210
|
|
|
211
|
+
const redbotType = String(redbot.type || '').trim().toLowerCase();
|
|
212
|
+
if ((redbotType === 'audio' || redbotType === 'voice') && Buffer.isBuffer(redbot.content)) {
|
|
213
|
+
const original = msg.originalMessage && typeof msg.originalMessage === 'object'
|
|
214
|
+
? msg.originalMessage
|
|
215
|
+
: {};
|
|
216
|
+
const voice = original.voice && typeof original.voice === 'object'
|
|
217
|
+
? original.voice
|
|
218
|
+
: original.message && original.message.voice && typeof original.message.voice === 'object'
|
|
219
|
+
? original.message.voice
|
|
220
|
+
: {};
|
|
221
|
+
msg.topic = 'ask';
|
|
222
|
+
msg.knxAi = Object.assign({}, msg.knxAi, {
|
|
223
|
+
sessionId: String(chatId),
|
|
224
|
+
voiceInput: {
|
|
225
|
+
source: 'telegram',
|
|
226
|
+
originalType: 'voice',
|
|
227
|
+
transport: 'redbot-buffer',
|
|
228
|
+
data: redbot.content,
|
|
229
|
+
fileId: String(voice.file_id || '').trim(),
|
|
230
|
+
mediaType: String(voice.mime_type || redbot.mimeType || 'audio/ogg'),
|
|
231
|
+
filename: String(voice.file_name || redbot.filename || 'telegram-voice.ogg'),
|
|
232
|
+
durationSeconds: Math.max(0, Number(voice.duration || redbot.duration) || 0),
|
|
233
|
+
fileSize: Math.max(0, Number(voice.file_size || redbot.fileSize) || redbot.content.length)
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
return msg;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const content = typeof redbot.content === 'string' ? redbot.content.trim() : '';
|
|
240
|
+
if (redbotType !== 'message' || content === '') return;
|
|
241
|
+
|
|
213
242
|
const action = content.toLowerCase();
|
|
214
243
|
if (action === 'confirm' || action === 'cancel') {
|
|
215
244
|
msg.topic = action;
|
|
@@ -300,6 +329,33 @@ if (image && Buffer.isBuffer(image.data)) {
|
|
|
300
329
|
return msg;
|
|
301
330
|
}
|
|
302
331
|
|
|
332
|
+
const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
|
|
333
|
+
const audio = msg.knxAi && msg.knxAi.audio;
|
|
334
|
+
if (audio && Buffer.isBuffer(audio.data) && !(confirmation && confirmation.required === true)) {
|
|
335
|
+
const voiceLanguage = String(msg.knxAi.language || (source && source.language) || '').trim().toLowerCase().split(/[-_]/)[0];
|
|
336
|
+
const disclosureLabels = {
|
|
337
|
+
de: 'KI-generierte Stimme',
|
|
338
|
+
en: 'AI-generated voice',
|
|
339
|
+
es: 'Voz generada por IA',
|
|
340
|
+
fr: 'Voix générée par l’IA',
|
|
341
|
+
it: 'Voce generata dall’IA',
|
|
342
|
+
zh: 'AI 生成的语音'
|
|
343
|
+
};
|
|
344
|
+
const disclosure = disclosureLabels[voiceLanguage] || disclosureLabels.en;
|
|
345
|
+
msg.payload = {
|
|
346
|
+
transport: sourcePayload.transport || 'telegram',
|
|
347
|
+
chatId: chatId,
|
|
348
|
+
type: 'audio',
|
|
349
|
+
inbound: false,
|
|
350
|
+
content: audio.data,
|
|
351
|
+
filename: audio.filename || 'knx-ai-reply.ogg',
|
|
352
|
+
mimeType: audio.mediaType || 'audio/ogg',
|
|
353
|
+
caption: [disclosure, content].filter(Boolean).join('\\n').slice(0, 1024)
|
|
354
|
+
};
|
|
355
|
+
if (sourcePayload.userId !== undefined) msg.payload.userId = sourcePayload.userId;
|
|
356
|
+
return msg;
|
|
357
|
+
}
|
|
358
|
+
|
|
303
359
|
const redbotPayload = {
|
|
304
360
|
transport: sourcePayload.transport || 'telegram',
|
|
305
361
|
chatId: chatId,
|
|
@@ -309,7 +365,6 @@ const redbotPayload = {
|
|
|
309
365
|
};
|
|
310
366
|
if (sourcePayload.userId !== undefined) redbotPayload.userId = sourcePayload.userId;
|
|
311
367
|
|
|
312
|
-
const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
|
|
313
368
|
if (confirmation && confirmation.required === true && Array.isArray(confirmation.actions)) {
|
|
314
369
|
redbotPayload.type = 'inline-buttons';
|
|
315
370
|
redbotPayload.buttons = confirmation.actions.map(function (action) {
|