node-red-contrib-knx-ultimate 6.3.29 → 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 +4 -0
- 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/utils/knxAiTelegramVoice.js +74 -4
- package/package.json +2 -2
- package/resources/KNXAIChatAdapterMappings.js +60 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 6.3.30** - August 2026<br/>
|
|
10
|
+
|
|
11
|
+
- **KNX AI — two-way RedBot Telegram voice chat**: the `RedBot / node-red-contrib-chatbot (Telegram)` preset now accepts RedBot's native inbound `audio` payload and its already-downloaded OGG/Opus `Buffer`, applies the same 20 MB and five-minute safeguards without downloading the file a second time, and transcribes it through the selected OpenAI-compatible provider. Successful requests receive a native RedBot `audio` reply with the localized AI-generated-voice disclosure and text caption; KNX confirmations deliberately remain RedBot `inline-buttons` text messages because its Telegram voice sender cannot attach that keyboard to `sendVoice`. Existing saved RedBot input/output mappings are upgraded at runtime. The separate `windkh/node-red-contrib-telegrambot` voice contract and its tests remain unchanged.<br/>
|
|
12
|
+
|
|
9
13
|
**Version 6.3.29** - August 2026<br/>
|
|
10
14
|
|
|
11
15
|
- **KNX AI — patient provider-neutral model requests**: every LLM chat request now receives the same 30-minute minimum deadline, regardless of provider, model or model manager, while longer legacy configured values remain honored. The model transport carries that deadline through both streaming and non-streaming responses instead of inheriting a shorter HTTP-library header timeout; fast responses are returned immediately, model-response timeouts remain distinct from unreachable-server errors, and Telegram audio operations retain their separate two-minute safeguard.<br/>
|
package/nodes/knxUltimateAI.js
CHANGED
|
@@ -834,6 +834,7 @@ const buildKnxAiSetupDoctorSnapshot = ({
|
|
|
834
834
|
if (apiKeyRequired && llm.apiKeyConfigured !== true) missing.push('API key')
|
|
835
835
|
const providerReady = missing.length === 0
|
|
836
836
|
const chatPreset = String(llm.chatAdapterPreset || 'none').trim() || 'none'
|
|
837
|
+
const telegramVoiceApplicable = ['windkh-telegrambot', 'redbot-telegram'].includes(chatPreset)
|
|
837
838
|
const chatPresetLabels = {
|
|
838
839
|
'windkh-telegrambot': 'Telegram Bot',
|
|
839
840
|
'redbot-telegram': 'RedBot Telegram'
|
|
@@ -903,7 +904,7 @@ const buildKnxAiSetupDoctorSnapshot = ({
|
|
|
903
904
|
{ id: 'chat', status: chatStatus, blocking: chatPreset !== 'none', weight: chatPreset !== 'none' ? 10 : 0, details: { preset: chatPreset, presetLabel: chatPresetLabels[chatPreset] || chatPreset, ready: chatVerified, wired: chatWired, upstreamCount, outputConnected: assistantOutput.connected === true } },
|
|
904
905
|
{ id: 'commands', status: commandStatus, blocking: llm.allowKnxCommands === true, weight: llm.allowKnxCommands === true ? 10 : 0, details: { enabled: llm.allowKnxCommands === true, connected: commandOutput.connected === true, verified: commandTargetVerified } },
|
|
905
906
|
{ id: 'tts', status: ttsOutput.connected === true ? 'pass' : 'info', blocking: false, weight: 0, details: { connected: ttsOutput.connected === true, connectionCount: Math.max(0, Number(ttsOutput.connectionCount) || 0) } },
|
|
906
|
-
{ id: 'voice', status:
|
|
907
|
+
{ id: 'voice', status: !telegramVoiceApplicable ? 'info' : provider === 'openai_compat' && providerReady ? 'pass' : 'warn', blocking: false, weight: 0, details: { applicable: telegramVoiceApplicable, ready: telegramVoiceApplicable && provider === 'openai_compat' && providerReady } },
|
|
907
908
|
{ id: 'cameras', status: Number(integrations.cameraCount) > 0 ? 'pass' : 'info', blocking: false, weight: 0, details: { cameraCount: Math.max(0, Number(integrations.cameraCount) || 0), adapterCount: Math.max(0, Number(integrations.cameraAdapterCount) || 0) } },
|
|
908
909
|
{ id: 'webAccess', status: webDetails.enabled ? 'pass' : 'info', blocking: false, weight: 0, details: webDetails },
|
|
909
910
|
{ id: 'proactiveWeb', status: proactiveWebStatus, blocking: false, weight: 0, details: proactiveWebDetails }
|
|
@@ -12741,7 +12742,7 @@ module.exports = function (RED) {
|
|
|
12741
12742
|
|
|
12742
12743
|
const prepareKnxAiTelegramVoiceInput = async (message) => {
|
|
12743
12744
|
if (!isKnxAiTelegramVoiceInput(message)) return message
|
|
12744
|
-
if (
|
|
12745
|
+
if (!['windkh-telegrambot', 'redbot-telegram'].includes(node.chatAdapterPreset)) return message
|
|
12745
12746
|
const voiceInput = Object.assign({}, message.knxAi.voiceInput)
|
|
12746
12747
|
redactKnxAiTelegramVoiceLocations(message)
|
|
12747
12748
|
if (!node.llmEnabled) {
|
|
@@ -12775,14 +12776,16 @@ module.exports = function (RED) {
|
|
|
12775
12776
|
})
|
|
12776
12777
|
delete safeVoiceInput.weblink
|
|
12777
12778
|
delete safeVoiceInput.path
|
|
12779
|
+
delete safeVoiceInput.data
|
|
12778
12780
|
message.knxAi = Object.assign({}, message.knxAi, { voiceInput: safeVoiceInput })
|
|
12779
12781
|
return message
|
|
12780
12782
|
}
|
|
12781
12783
|
|
|
12782
12784
|
const enrichKnxAiTelegramVoiceReplyMetadata = async ({ inputMessage, content, speechContent, metadata = {} } = {}) => {
|
|
12783
12785
|
const enriched = Object.assign({}, metadata)
|
|
12784
|
-
if (!isKnxAiTelegramVoiceInput(inputMessage) ||
|
|
12786
|
+
if (!isKnxAiTelegramVoiceInput(inputMessage) || !['windkh-telegrambot', 'redbot-telegram'].includes(node.chatAdapterPreset)) return enriched
|
|
12785
12787
|
if (enriched.image && enriched.image.data) return enriched
|
|
12788
|
+
if (node.chatAdapterPreset === 'redbot-telegram' && enriched.confirmationRequest && enriched.confirmationRequest.required === true) return enriched
|
|
12786
12789
|
let speechText = speechContent === undefined ? content : speechContent
|
|
12787
12790
|
if (speechText && typeof speechText === 'object') {
|
|
12788
12791
|
speechText = speechText.error || speechText.message || safeStringify(speechText)
|
|
@@ -78,7 +78,7 @@ Mit dieser Vorlage wird eine Telegram-Sprachnachricht (`msg.payload.type = "voic
|
|
|
78
78
|
|
|
79
79
|
Jede native Sprachantwort beginnt in der Bildunterschrift mit dem lokalisierten Hinweis **KI-generierte Stimme**, der für den Telegram-Empfänger sichtbar ist.
|
|
80
80
|
|
|
81
|
-
Die enthaltene Vorlage **RedBot / node-red-contrib-chatbot (Telegram)** folgt dem gemeinsamen RedBot-Nachrichtenformat. Verbinden Sie `chatbot-telegram-receive` direkt mit KNX AI und Ausgang 3 direkt mit `chatbot-telegram-send`; ein separater Callback-Node ist nicht erforderlich, da RedBot Postbacks von Inline-Schaltflächen in normale Eingangsnachrichten umwandelt.
|
|
81
|
+
Die enthaltene Vorlage **RedBot / node-red-contrib-chatbot (Telegram)** folgt dem gemeinsamen RedBot-Nachrichtenformat. Verbinden Sie `chatbot-telegram-receive` direkt mit KNX AI und Ausgang 3 direkt mit `chatbot-telegram-send`; ein separater Callback-Node ist nicht erforderlich, da RedBot Postbacks von Inline-Schaltflächen in normale Eingangsnachrichten umwandelt. Text und Postbacks verwenden RedBots `message`-Payload. Eine native Telegram-Sprachnachricht kommt als `type = "audio"` mit dem bereits von RedBot heruntergeladenen OGG/Opus-`Buffer` an: KNX AI wendet Größen- und Dauergrenzen ohne zweiten Download an, transkribiert sie mit demselben oben beschriebenen OpenAI-kompatiblen Provider und antwortet mit einem nativen RedBot-`audio`-Payload samt Textunterschrift und lokalisiertem Hinweis auf die KI-generierte Stimme. Wenn eine KNX-Schreibbestätigung erforderlich ist, bleibt die Antwort bewusst ein textbasierter `inline-buttons`-Payload, da RedBot diese Schaltflächen nicht an dieselbe Sprachnachricht anhängen kann. Die Ausgangszuordnung bewahrt die RedBot-Trackingdaten `originalMessage`, `chat`, `api` und `client`; ältere gespeicherte RedBot-Zuordnungen werden zur Laufzeit aktualisiert. RedBot bleibt eine separate optionale Abhängigkeit.
|
|
82
82
|
|
|
83
83
|
### Automatisch erkannte Kamera-Adapter
|
|
84
84
|
Installierte Kamerapakete können KNX AI zur Laufzeit einen Kamera-Adapter bereitstellen. Es gibt weder eine Auswahl noch einen Kamera-Node, der mit KNX AI verbunden werden muss: verfügbare Adapter, Controller und Kameras werden automatisch erkannt und in den Chat-Kontext aufgenommen. `node-red-contrib-unifi-ultimate` ist der erste unterstützte Anbieter; weitere Pakete wie `hikvision-ultimate` können sich über denselben herstellerneutralen Vertrag registrieren.
|
|
@@ -78,7 +78,7 @@ With this preset, a Telegram voice message (`msg.payload.type = "voice"`) is han
|
|
|
78
78
|
|
|
79
79
|
Every native voice caption begins with a localized **AI-generated voice** disclosure for the Telegram recipient.
|
|
80
80
|
|
|
81
|
-
The included **RedBot / node-red-contrib-chatbot (Telegram)** preset follows RedBot's common message contract. Connect `chatbot-telegram-receive` directly to KNX AI and output 3 directly to `chatbot-telegram-send`; no separate callback node is needed because RedBot converts inline-button postbacks into normal inbound messages.
|
|
81
|
+
The included **RedBot / node-red-contrib-chatbot (Telegram)** preset follows RedBot's common message contract. Connect `chatbot-telegram-receive` directly to KNX AI and output 3 directly to `chatbot-telegram-send`; no separate callback node is needed because RedBot converts inline-button postbacks into normal inbound messages. Text and postbacks use RedBot's `message` payload. A native Telegram voice arrives as `type = "audio"` with an OGG/Opus `Buffer` already downloaded by RedBot; KNX AI applies its size and duration limits without downloading it again, transcribes it with the same OpenAI-compatible provider described above, and returns a native RedBot `audio` reply with the localized AI-generated-voice disclosure and text caption. When a KNX write needs confirmation, the reply deliberately remains a text `inline-buttons` payload because RedBot cannot attach those buttons to the same voice message. The output mapping preserves RedBot's `originalMessage`, `chat`, `api`, and `client` tracking data, and older saved RedBot mappings are upgraded at runtime. RedBot remains a separate optional dependency.
|
|
82
82
|
|
|
83
83
|
### Automatically detected camera adapters
|
|
84
84
|
Installed camera packages can publish a camera adapter to KNX AI at runtime. There is no selector and no camera node to wire to KNX AI: available adapters, controllers and cameras are detected automatically and included in the chat context. `node-red-contrib-unifi-ultimate` is the first supported provider; other packages, such as `hikvision-ultimate`, can register through the same vendor-neutral contract.
|
|
@@ -78,7 +78,7 @@ Con este preajuste, los mensajes de voz de Telegram (`msg.payload.type = "voice"
|
|
|
78
78
|
|
|
79
79
|
La leyenda de cada respuesta de voz nativa comienza con el aviso localizado **Voz generada por IA**, visible para el destinatario de Telegram.
|
|
80
80
|
|
|
81
|
-
El preajuste incluido **RedBot / node-red-contrib-chatbot (Telegram)** sigue el formato común de mensajes de RedBot. Conecta directamente `chatbot-telegram-receive` a KNX AI y la salida 3 a `chatbot-telegram-send`; no hace falta un nodo callback separado porque RedBot convierte los postbacks de los botones inline en mensajes de entrada normales. El
|
|
81
|
+
El preajuste incluido **RedBot / node-red-contrib-chatbot (Telegram)** sigue el formato común de mensajes de RedBot. Conecta directamente `chatbot-telegram-receive` a KNX AI y la salida 3 a `chatbot-telegram-send`; no hace falta un nodo callback separado porque RedBot convierte los postbacks de los botones inline en mensajes de entrada normales. El texto y los postbacks usan el payload RedBot `message`. Un mensaje de voz nativo de Telegram llega como `type = "audio"` con el `Buffer` OGG/Opus ya descargado por RedBot: KNX AI aplica los límites de tamaño y duración sin volver a descargarlo, lo transcribe con el mismo proveedor OpenAI-compatible descrito arriba y responde con un payload RedBot `audio` nativo, el texto y el aviso localizado de voz generada por IA. Cuando una escritura KNX necesita confirmación, la respuesta se mantiene deliberadamente como payload de texto `inline-buttons`, porque RedBot no puede adjuntar esos botones al mismo mensaje de voz. El mapeo de salida conserva los datos de seguimiento RedBot `originalMessage`, `chat`, `api` y `client`; los mapeos RedBot antiguos guardados también se actualizan en tiempo de ejecución. RedBot sigue siendo una dependencia opcional separada.
|
|
82
82
|
|
|
83
83
|
### Adaptadores de cámara detectados automáticamente
|
|
84
84
|
Los paquetes de cámaras instalados pueden publicar en tiempo de ejecución un adaptador para KNX AI. No hay selector ni nodo de cámara que conectar a KNX AI: los adaptadores, controladores y cámaras disponibles se detectan automáticamente y se incorporan al contexto del chat. `node-red-contrib-unifi-ultimate` es el primer proveedor compatible; otros paquetes, como `hikvision-ultimate`, pueden registrarse mediante el mismo contrato independiente del fabricante.
|
|
@@ -78,7 +78,7 @@ Avec ce préréglage, un message vocal Telegram (`msg.payload.type = "voice"`) n
|
|
|
78
78
|
|
|
79
79
|
La légende de chaque réponse vocale native commence par la mention localisée **Voix générée par l’IA**, visible par le destinataire Telegram.
|
|
80
80
|
|
|
81
|
-
Le préréglage inclus **RedBot / node-red-contrib-chatbot (Telegram)** suit le format de message commun de RedBot. Connectez directement `chatbot-telegram-receive` à KNX AI et la sortie 3 à `chatbot-telegram-send` ; aucun nœud de callback séparé n’est nécessaire, car RedBot convertit les postbacks des boutons inline en messages entrants ordinaires. Le
|
|
81
|
+
Le préréglage inclus **RedBot / node-red-contrib-chatbot (Telegram)** suit le format de message commun de RedBot. Connectez directement `chatbot-telegram-receive` à KNX AI et la sortie 3 à `chatbot-telegram-send` ; aucun nœud de callback séparé n’est nécessaire, car RedBot convertit les postbacks des boutons inline en messages entrants ordinaires. Le texte et les postbacks utilisent le payload RedBot `message`. Un message vocal Telegram natif arrive avec `type = "audio"` et le `Buffer` OGG/Opus déjà téléchargé par RedBot : KNX AI applique les limites de taille et de durée sans second téléchargement, le transcrit avec le même fournisseur OpenAI-compatible décrit ci-dessus et répond avec un payload RedBot `audio` natif, sa légende textuelle et la mention localisée de voix générée par l’IA. Lorsqu’une écriture KNX exige une confirmation, la réponse reste volontairement un payload texte `inline-buttons`, car RedBot ne peut pas joindre ces boutons au même message vocal. Le mappage de sortie conserve les données de suivi RedBot `originalMessage`, `chat`, `api` et `client` ; les anciens mappages RedBot enregistrés sont également mis à niveau à l’exécution. RedBot reste une dépendance optionnelle distincte.
|
|
82
82
|
|
|
83
83
|
### Adaptateurs de caméra détectés automatiquement
|
|
84
84
|
Les paquets de caméra installés peuvent publier à l’exécution un adaptateur pour KNX AI. Il n’existe aucun sélecteur ni nœud caméra à relier à KNX AI : les adaptateurs, contrôleurs et caméras disponibles sont détectés automatiquement et ajoutés au contexte du chat. `node-red-contrib-unifi-ultimate` est le premier fournisseur pris en charge ; d’autres paquets, tels que `hikvision-ultimate`, peuvent s’enregistrer avec le même contrat indépendant du fabricant.
|
|
@@ -78,7 +78,7 @@ Con questo preset, un messaggio vocale Telegram (`msg.payload.type = "voice"`) v
|
|
|
78
78
|
|
|
79
79
|
La didascalia di ogni vocale nativo inizia con l'indicazione localizzata **Voce generata dall’IA**, visibile al destinatario Telegram.
|
|
80
80
|
|
|
81
|
-
Il preset incluso **RedBot / node-red-contrib-chatbot (Telegram)** segue il formato comune dei messaggi RedBot. Collega direttamente `chatbot-telegram-receive` a KNX AI e l'uscita 3 direttamente a `chatbot-telegram-send`; non serve un nodo callback separato perché RedBot converte i postback dei pulsanti inline in normali messaggi in ingresso.
|
|
81
|
+
Il preset incluso **RedBot / node-red-contrib-chatbot (Telegram)** segue il formato comune dei messaggi RedBot. Collega direttamente `chatbot-telegram-receive` a KNX AI e l'uscita 3 direttamente a `chatbot-telegram-send`; non serve un nodo callback separato perché RedBot converte i postback dei pulsanti inline in normali messaggi in ingresso. Testo e postback usano il payload RedBot `message`. Un vocale Telegram nativo arriva come `type = "audio"` con il `Buffer` OGG/Opus già scaricato da RedBot: KNX AI applica i limiti di dimensione e durata senza scaricarlo una seconda volta, lo trascrive con lo stesso provider OpenAI-compatible descritto sopra e risponde con un payload RedBot `audio` nativo, didascalia testuale e indicazione localizzata della voce generata dall'IA. Quando una scrittura KNX richiede conferma, la risposta resta intenzionalmente un payload testuale `inline-buttons`, perché RedBot non può allegare quei pulsanti allo stesso messaggio vocale. La mappatura d'uscita conserva i dati di tracciamento RedBot `originalMessage`, `chat`, `api` e `client`; anche le vecchie mappature RedBot salvate vengono aggiornate a runtime. RedBot resta una dipendenza opzionale separata.
|
|
82
82
|
|
|
83
83
|
### Adapter telecamera rilevati automaticamente
|
|
84
84
|
I pacchetti di telecamere installati possono pubblicare a runtime un adapter per KNX AI. Non esistono selettori né nodi telecamera da collegare a KNX AI: adapter, controller e telecamere disponibili vengono rilevati automaticamente e inseriti nel contesto della chat. `node-red-contrib-unifi-ultimate` è il primo provider supportato; altri pacchetti, come `hikvision-ultimate`, possono registrarsi tramite lo stesso contratto indipendente dal produttore.
|
|
@@ -78,7 +78,7 @@ Canvas 上的节点状态专门用于显示最近收到的请求,以及 LLM
|
|
|
78
78
|
|
|
79
79
|
每条原生语音回复的文字说明都会以本地化的 **AI 生成的语音** 提示开头,Telegram 收件人可以看到该提示。
|
|
80
80
|
|
|
81
|
-
随附的 **RedBot / node-red-contrib-chatbot (Telegram)** 预设遵循 RedBot 的通用消息格式。将 `chatbot-telegram-receive` 直接连接到 KNX AI,并将输出 3 直接连接到 `chatbot-telegram-send`;无需单独的 callback 节点,因为 RedBot 会把内联按钮的 postback
|
|
81
|
+
随附的 **RedBot / node-red-contrib-chatbot (Telegram)** 预设遵循 RedBot 的通用消息格式。将 `chatbot-telegram-receive` 直接连接到 KNX AI,并将输出 3 直接连接到 `chatbot-telegram-send`;无需单独的 callback 节点,因为 RedBot 会把内联按钮的 postback 转换成普通入站消息。文本和 postback 使用 RedBot 的 `message` payload。Telegram 原生语音以 `type = "audio"` 到达,其中包含已由 RedBot 下载的 OGG/Opus `Buffer`;KNX AI 无需再次下载即可执行大小和时长限制,使用上文所述的同一个 OpenAI-compatible 提供商进行转录,并返回带文字说明和本地化 AI 生成语音提示的原生 RedBot `audio` payload。当 KNX 写入需要确认时,回复会有意保留为文本 `inline-buttons` payload,因为 RedBot 无法把这些按钮附加到同一条语音消息。输出映射会保留 RedBot 的 `originalMessage`、`chat`、`api` 和 `client` 跟踪数据;旧的已保存 RedBot 映射也会在运行时升级。RedBot 仍是独立的可选依赖项。
|
|
82
82
|
|
|
83
83
|
### 自动检测的摄像机适配器
|
|
84
84
|
已安装的摄像机软件包可以在运行时向 KNX AI 发布适配器。无需选择器,也无需将摄像机节点连接到 KNX AI:可用的适配器、控制器和摄像机会被自动检测并加入聊天上下文。`node-red-contrib-unifi-ultimate` 是首个受支持的提供方;`hikvision-ultimate` 等其他软件包可通过同一套厂商无关协议注册。
|
|
@@ -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/",
|
|
@@ -150,4 +150,4 @@
|
|
|
150
150
|
"vite": "^7.3.6",
|
|
151
151
|
"vue": "^3.5.41"
|
|
152
152
|
}
|
|
153
|
-
}
|
|
153
|
+
}
|
|
@@ -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) {
|