node-red-contrib-knx-ultimate 4.3.6 → 4.3.8
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 +11 -0
- package/nodes/knxUltimate-config.js +45 -10
- package/nodes/knxUltimateAI.html +87 -106
- package/nodes/knxUltimateAI.js +527 -23
- package/nodes/locales/de/knxUltimateAI.html +18 -11
- package/nodes/locales/de/knxUltimateAI.json +9 -12
- package/nodes/locales/en/knxUltimateAI.html +18 -11
- package/nodes/locales/en/knxUltimateAI.json +9 -12
- package/nodes/locales/es/knxUltimateAI.html +18 -11
- package/nodes/locales/es/knxUltimateAI.json +9 -12
- package/nodes/locales/fr/knxUltimateAI.html +18 -11
- package/nodes/locales/fr/knxUltimateAI.json +9 -12
- package/nodes/locales/it/knxUltimateAI.html +18 -11
- package/nodes/locales/it/knxUltimateAI.json +9 -12
- package/nodes/locales/zh-CN/knxUltimateAI.html +18 -11
- package/nodes/locales/zh-CN/knxUltimateAI.json +9 -12
- package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -1
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +1 -1
- package/package.json +1 -1
package/nodes/knxUltimateAI.js
CHANGED
|
@@ -424,6 +424,8 @@ const computeAnomalySeverity = (payload) => {
|
|
|
424
424
|
|
|
425
425
|
const SVG_REQUEST_RE = /\b(svg|chart|graph|plot|diagram|bar|pie|line|grafico|grafici|diagramma|istogramma|torta)\b/i
|
|
426
426
|
const SVG_PRESENT_RE = /```svg[\s\S]*?```|<svg[\s>][\s\S]*?<\/svg>/i
|
|
427
|
+
const FUNCTION_NODE_CODE_REVIEW_RE = /\b(function|function node|nodo function|nodi function)\b/i
|
|
428
|
+
const JAVASCRIPT_REVIEW_RE = /\b(js|javascript|java\s*script|code|codice|script|sorgente|source|errore|errori|error|bug|review|reviewa|analizza|analy(?:s|z)e|check|controlla|debug)\b/i
|
|
427
429
|
|
|
428
430
|
const escapeXml = (value) => String(value || '')
|
|
429
431
|
.replace(/&/g, '&')
|
|
@@ -440,6 +442,17 @@ const truncateLabel = (value, maxLen = 14) => {
|
|
|
440
442
|
|
|
441
443
|
const shouldGenerateSvgChart = (question) => SVG_REQUEST_RE.test(String(question || ''))
|
|
442
444
|
|
|
445
|
+
const shouldIncludeFunctionNodeSourceContext = (question) => {
|
|
446
|
+
const q = String(question || '').trim()
|
|
447
|
+
if (!q) return false
|
|
448
|
+
return FUNCTION_NODE_CODE_REVIEW_RE.test(q) && JAVASCRIPT_REVIEW_RE.test(q)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const normalizeCodeBlockText = (value) => String(value || '')
|
|
452
|
+
.replace(/\r\n/g, '\n')
|
|
453
|
+
.replace(/\r/g, '\n')
|
|
454
|
+
.trim()
|
|
455
|
+
|
|
443
456
|
const stripPayloadDecimals = (value) => {
|
|
444
457
|
if (value === undefined || value === null) return value
|
|
445
458
|
if (typeof value === 'number') {
|
|
@@ -875,6 +888,106 @@ const readJsonFileSafe = (filePath, fallbackValue) => {
|
|
|
875
888
|
}
|
|
876
889
|
}
|
|
877
890
|
|
|
891
|
+
const formatArchiveDayKey = (ts) => {
|
|
892
|
+
try {
|
|
893
|
+
return new Date(ts).toISOString().slice(0, 10)
|
|
894
|
+
} catch (error) {
|
|
895
|
+
return new Date().toISOString().slice(0, 10)
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const collectArchiveDayKeysBetween = ({ fromTs, toTs }) => {
|
|
900
|
+
const out = []
|
|
901
|
+
const start = Number(fromTs || 0)
|
|
902
|
+
const end = Number(toTs || 0)
|
|
903
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return out
|
|
904
|
+
const cursor = new Date(start)
|
|
905
|
+
cursor.setUTCHours(0, 0, 0, 0)
|
|
906
|
+
const endDay = new Date(end)
|
|
907
|
+
endDay.setUTCHours(0, 0, 0, 0)
|
|
908
|
+
while (cursor.getTime() <= endDay.getTime()) {
|
|
909
|
+
out.push(cursor.toISOString().slice(0, 10))
|
|
910
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1)
|
|
911
|
+
}
|
|
912
|
+
return out
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const startOfLocalDayMs = (ts) => {
|
|
916
|
+
const d = new Date(ts)
|
|
917
|
+
d.setHours(0, 0, 0, 0)
|
|
918
|
+
return d.getTime()
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const endOfLocalDayMs = (ts) => {
|
|
922
|
+
const d = new Date(ts)
|
|
923
|
+
d.setHours(23, 59, 59, 999)
|
|
924
|
+
return d.getTime()
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const parseQuestionTimeRange = (question, nowTs = Date.now()) => {
|
|
928
|
+
const text = String(question || '').trim().toLowerCase()
|
|
929
|
+
if (!text) return null
|
|
930
|
+
|
|
931
|
+
const exactDates = Array.from(text.matchAll(/\b(\d{4}-\d{2}-\d{2})\b/g)).map(match => match[1])
|
|
932
|
+
if (exactDates.length >= 2) {
|
|
933
|
+
const start = new Date(`${exactDates[0]}T00:00:00`).getTime()
|
|
934
|
+
const end = new Date(`${exactDates[1]}T23:59:59.999`).getTime()
|
|
935
|
+
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
|
|
936
|
+
return { fromTs: start, toTs: end, label: `${exactDates[0]}..${exactDates[1]}`, explicit: true }
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (exactDates.length === 1) {
|
|
940
|
+
const start = new Date(`${exactDates[0]}T00:00:00`).getTime()
|
|
941
|
+
const end = new Date(`${exactDates[0]}T23:59:59.999`).getTime()
|
|
942
|
+
if (Number.isFinite(start) && Number.isFinite(end)) {
|
|
943
|
+
return { fromTs: start, toTs: end, label: exactDates[0], explicit: true }
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const dayStart = startOfLocalDayMs(nowTs)
|
|
948
|
+
const dayEnd = endOfLocalDayMs(nowTs)
|
|
949
|
+
const yesterdayStart = dayStart - (24 * 60 * 60 * 1000)
|
|
950
|
+
const yesterdayEnd = dayStart - 1
|
|
951
|
+
|
|
952
|
+
if (/\b(stamattina|this morning)\b/.test(text)) {
|
|
953
|
+
return { fromTs: dayStart, toTs: Math.min(dayEnd, dayStart + (12 * 60 * 60 * 1000) - 1), label: 'this morning', explicit: true }
|
|
954
|
+
}
|
|
955
|
+
if (/\b(oggi pomeriggio|this afternoon)\b/.test(text)) {
|
|
956
|
+
return { fromTs: dayStart + (12 * 60 * 60 * 1000), toTs: Math.min(dayEnd, dayStart + (18 * 60 * 60 * 1000) - 1), label: 'this afternoon', explicit: true }
|
|
957
|
+
}
|
|
958
|
+
if (/\b(stasera|this evening|tonight)\b/.test(text)) {
|
|
959
|
+
return { fromTs: dayStart + (18 * 60 * 60 * 1000), toTs: dayEnd, label: 'this evening', explicit: true }
|
|
960
|
+
}
|
|
961
|
+
if (/\b(oggi|today)\b/.test(text)) {
|
|
962
|
+
return { fromTs: dayStart, toTs: dayEnd, label: 'today', explicit: true }
|
|
963
|
+
}
|
|
964
|
+
if (/\b(ieri|yesterday)\b/.test(text)) {
|
|
965
|
+
return { fromTs: yesterdayStart, toTs: yesterdayEnd, label: 'yesterday', explicit: true }
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const lastDaysMatch = text.match(/\b(?:last|ultimi)\s+(\d{1,3})\s+(?:day|days|giorno|giorni)\b/)
|
|
969
|
+
if (lastDaysMatch) {
|
|
970
|
+
const days = Math.max(1, Number(lastDaysMatch[1] || 1))
|
|
971
|
+
return {
|
|
972
|
+
fromTs: nowTs - (days * 24 * 60 * 60 * 1000),
|
|
973
|
+
toTs: nowTs,
|
|
974
|
+
label: `last ${days} days`,
|
|
975
|
+
explicit: true
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (/\b(this week|questa settimana)\b/.test(text)) {
|
|
980
|
+
const d = new Date(nowTs)
|
|
981
|
+
const dow = d.getDay()
|
|
982
|
+
const offset = dow === 0 ? 6 : dow - 1
|
|
983
|
+
d.setHours(0, 0, 0, 0)
|
|
984
|
+
d.setDate(d.getDate() - offset)
|
|
985
|
+
return { fromTs: d.getTime(), toTs: nowTs, label: 'this week', explicit: true }
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return null
|
|
989
|
+
}
|
|
990
|
+
|
|
878
991
|
const normalizeAreaOverridePayload = (payload) => {
|
|
879
992
|
const p = payload && typeof payload === 'object' ? payload : {}
|
|
880
993
|
const normalized = {}
|
|
@@ -2602,13 +2715,14 @@ module.exports = function (RED) {
|
|
|
2602
2715
|
return catalog
|
|
2603
2716
|
}
|
|
2604
2717
|
|
|
2605
|
-
const
|
|
2718
|
+
const buildKnxUltimateProjectInventory = () => {
|
|
2606
2719
|
const tabById = new Map()
|
|
2607
2720
|
const gatewaysById = new Map()
|
|
2608
|
-
const
|
|
2721
|
+
const flowNodes = []
|
|
2609
2722
|
|
|
2610
2723
|
try {
|
|
2611
2724
|
if (typeof RED.nodes.eachNode !== 'function') return ''
|
|
2725
|
+
const gaRe = /\b\d{1,3}\/\d{1,3}\/\d{1,3}\b/g
|
|
2612
2726
|
|
|
2613
2727
|
// First pass: collect tabs + gateways
|
|
2614
2728
|
RED.nodes.eachNode((n) => {
|
|
@@ -2627,11 +2741,11 @@ module.exports = function (RED) {
|
|
|
2627
2741
|
}
|
|
2628
2742
|
})
|
|
2629
2743
|
|
|
2630
|
-
// Second pass: collect
|
|
2744
|
+
// Second pass: collect all flow nodes that may help the LLM understand KNX logic.
|
|
2631
2745
|
RED.nodes.eachNode((n) => {
|
|
2632
2746
|
if (!n || typeof n !== 'object') return
|
|
2633
2747
|
const type = String(n.type || '')
|
|
2634
|
-
if (
|
|
2748
|
+
if (type === 'tab' || type === 'subflow' || type === 'knxUltimate-config') return
|
|
2635
2749
|
|
|
2636
2750
|
const tabId = String(n.z || '')
|
|
2637
2751
|
const tabLabel = tabById.get(tabId) || ''
|
|
@@ -2640,6 +2754,16 @@ module.exports = function (RED) {
|
|
|
2640
2754
|
const server = String(n.server || '')
|
|
2641
2755
|
const gw = gatewaysById.get(server) || null
|
|
2642
2756
|
|
|
2757
|
+
const gaRefs = new Set()
|
|
2758
|
+
extractGAsFromValue({ value: n, outSet: gaRefs, gaRe, maxItems: 24 })
|
|
2759
|
+
const gaList = Array.from(gaRefs.values()).slice(0, 24)
|
|
2760
|
+
|
|
2761
|
+
const shortenSnippet = (value, maxLen = 140) => {
|
|
2762
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim()
|
|
2763
|
+
if (!text) return ''
|
|
2764
|
+
return text.length > maxLen ? `${text.slice(0, Math.max(0, maxLen - 3))}...` : text
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2643
2767
|
const entry = {
|
|
2644
2768
|
tabLabel,
|
|
2645
2769
|
type,
|
|
@@ -2648,7 +2772,12 @@ module.exports = function (RED) {
|
|
|
2648
2772
|
gatewayId: server,
|
|
2649
2773
|
gatewayName: gw ? gw.name : '',
|
|
2650
2774
|
topic: n.topic !== undefined ? String(n.topic) : '',
|
|
2651
|
-
dpt: n.dpt !== undefined ? String(n.dpt) : ''
|
|
2775
|
+
dpt: n.dpt !== undefined ? String(n.dpt) : '',
|
|
2776
|
+
gaRefs: gaList,
|
|
2777
|
+
payload: n.payload !== undefined ? shortenSnippet(n.payload, 80) : '',
|
|
2778
|
+
payloadType: n.payloadType !== undefined ? String(n.payloadType) : '',
|
|
2779
|
+
outputTopic: n.outputtopic !== undefined ? String(n.outputtopic) : '',
|
|
2780
|
+
setTopicType: n.setTopicType !== undefined ? String(n.setTopicType) : ''
|
|
2652
2781
|
}
|
|
2653
2782
|
|
|
2654
2783
|
if (type === 'knxUltimate') {
|
|
@@ -2669,17 +2798,31 @@ module.exports = function (RED) {
|
|
|
2669
2798
|
entry.gaRewriteRules = n.gaRewriteRules !== undefined ? String(n.gaRewriteRules) : ''
|
|
2670
2799
|
entry.rewriteSource = n.rewriteSource === true || n.rewriteSource === 'true'
|
|
2671
2800
|
entry.srcRewriteRules = n.srcRewriteRules !== undefined ? String(n.srcRewriteRules) : ''
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
|
|
2801
|
+
} else if (type === 'function') {
|
|
2802
|
+
entry.funcSnippet = shortenSnippet(n.func, 220)
|
|
2803
|
+
} else if (type === 'change') {
|
|
2804
|
+
entry.rulesSnippet = shortenSnippet(safeStringify(n.rules), 180)
|
|
2805
|
+
} else if (type === 'inject') {
|
|
2806
|
+
entry.injectOnce = n.once === true || n.once === 'true'
|
|
2807
|
+
entry.repeat = n.repeat !== undefined ? String(n.repeat) : ''
|
|
2808
|
+
entry.crontab = n.crontab !== undefined ? String(n.crontab) : ''
|
|
2809
|
+
} else if (type === 'template') {
|
|
2810
|
+
entry.templateSnippet = shortenSnippet(n.template, 180)
|
|
2811
|
+
} else if (type === 'switch') {
|
|
2812
|
+
entry.rulesSnippet = shortenSnippet(safeStringify(n.rules), 180)
|
|
2813
|
+
} else if (type === 'api-current-state' || type === 'server-state-changed') {
|
|
2814
|
+
entry.entityId = n.entityid !== undefined ? String(n.entityid) : ''
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
flowNodes.push(entry)
|
|
2675
2818
|
})
|
|
2676
2819
|
} catch (error) {
|
|
2677
2820
|
return ''
|
|
2678
2821
|
}
|
|
2679
2822
|
|
|
2680
|
-
if (!
|
|
2823
|
+
if (!flowNodes.length && !gatewaysById.size) return ''
|
|
2681
2824
|
|
|
2682
|
-
const sorted =
|
|
2825
|
+
const sorted = flowNodes
|
|
2683
2826
|
.sort((a, b) => {
|
|
2684
2827
|
const at = (a.tabLabel || '').localeCompare(b.tabLabel || '')
|
|
2685
2828
|
if (at !== 0) return at
|
|
@@ -2687,13 +2830,12 @@ module.exports = function (RED) {
|
|
|
2687
2830
|
if (an !== 0) return an
|
|
2688
2831
|
return (a.type || '').localeCompare(b.type || '')
|
|
2689
2832
|
})
|
|
2690
|
-
.slice(0, Math.max(0, Number(maxNodes) || 0))
|
|
2691
2833
|
|
|
2692
2834
|
const shorten = (id) => (id && id.length > 8) ? id.slice(0, 8) : id
|
|
2693
2835
|
const safeLine = (s) => String(s || '').replace(/\s+/g, ' ').trim()
|
|
2694
2836
|
|
|
2695
2837
|
const lines = []
|
|
2696
|
-
lines.push('Node-RED
|
|
2838
|
+
lines.push('Node-RED project inventory:')
|
|
2697
2839
|
|
|
2698
2840
|
if (gatewaysById.size) {
|
|
2699
2841
|
lines.push(`Gateways (knxUltimate-config): ${gatewaysById.size}`)
|
|
@@ -2707,7 +2849,7 @@ module.exports = function (RED) {
|
|
|
2707
2849
|
if (gatewaysById.size > 20) lines.push('- ...')
|
|
2708
2850
|
}
|
|
2709
2851
|
|
|
2710
|
-
lines.push(`
|
|
2852
|
+
lines.push(`Project nodes: ${flowNodes.length}`)
|
|
2711
2853
|
for (const n of sorted) {
|
|
2712
2854
|
const parts = []
|
|
2713
2855
|
if (n.tabLabel) parts.push(`[${safeLine(n.tabLabel)}]`)
|
|
@@ -2716,6 +2858,7 @@ module.exports = function (RED) {
|
|
|
2716
2858
|
if (n.name) parts.push(`name="${safeLine(n.name)}"`)
|
|
2717
2859
|
if (n.gatewayName) parts.push(`gw="${safeLine(n.gatewayName)}"`)
|
|
2718
2860
|
if (!n.gatewayName && n.gatewayId) parts.push(`gwId=${shorten(n.gatewayId)}`)
|
|
2861
|
+
if (Array.isArray(n.gaRefs) && n.gaRefs.length) parts.push(`gaRefs="${safeLine(n.gaRefs.join(','))}"`)
|
|
2719
2862
|
|
|
2720
2863
|
if (n.type === 'knxUltimate') {
|
|
2721
2864
|
if (n.topic) parts.push(`topic=${safeLine(n.topic)}`)
|
|
@@ -2733,8 +2876,26 @@ module.exports = function (RED) {
|
|
|
2733
2876
|
if (n.gaRewriteRules) parts.push(`gaRewriteRules="${safeLine(n.gaRewriteRules)}"`)
|
|
2734
2877
|
if (n.rewriteSource) parts.push('rewriteSource=true')
|
|
2735
2878
|
if (n.srcRewriteRules) parts.push(`srcRewriteRules="${safeLine(n.srcRewriteRules)}"`)
|
|
2879
|
+
} else if (n.type === 'function') {
|
|
2880
|
+
if (n.funcSnippet) parts.push(`func="${safeLine(n.funcSnippet)}"`)
|
|
2881
|
+
} else if (n.type === 'change' || n.type === 'switch') {
|
|
2882
|
+
if (n.rulesSnippet) parts.push(`rules="${safeLine(n.rulesSnippet)}"`)
|
|
2883
|
+
} else if (n.type === 'inject') {
|
|
2884
|
+
if (n.topic) parts.push(`topic=${safeLine(n.topic)}`)
|
|
2885
|
+
if (n.payload) parts.push(`payload="${safeLine(n.payload)}"`)
|
|
2886
|
+
if (n.payloadType) parts.push(`payloadType=${safeLine(n.payloadType)}`)
|
|
2887
|
+
if (n.injectOnce) parts.push('once=true')
|
|
2888
|
+
if (n.repeat) parts.push(`repeat=${safeLine(n.repeat)}`)
|
|
2889
|
+
if (n.crontab) parts.push(`crontab="${safeLine(n.crontab)}"`)
|
|
2890
|
+
} else if (n.type === 'template') {
|
|
2891
|
+
if (n.templateSnippet) parts.push(`template="${safeLine(n.templateSnippet)}"`)
|
|
2892
|
+
} else if (n.type === 'api-current-state' || n.type === 'server-state-changed') {
|
|
2893
|
+
if (n.entityId) parts.push(`entityId=${safeLine(n.entityId)}`)
|
|
2736
2894
|
} else {
|
|
2737
2895
|
if (n.topic) parts.push(`topic=${safeLine(n.topic)}`)
|
|
2896
|
+
if (n.payload) parts.push(`payload="${safeLine(n.payload)}"`)
|
|
2897
|
+
if (n.outputTopic) parts.push(`outputTopic=${safeLine(n.outputTopic)}`)
|
|
2898
|
+
if (n.setTopicType) parts.push(`setTopicType=${safeLine(n.setTopicType)}`)
|
|
2738
2899
|
}
|
|
2739
2900
|
lines.push(`- ${parts.join(' ')}`)
|
|
2740
2901
|
}
|
|
@@ -2742,6 +2903,147 @@ module.exports = function (RED) {
|
|
|
2742
2903
|
return lines.join('\n').trim()
|
|
2743
2904
|
}
|
|
2744
2905
|
|
|
2906
|
+
const buildFunctionNodeSourceContext = ({ maxChars = 12000, maxNodes = 12 } = {}) => {
|
|
2907
|
+
try {
|
|
2908
|
+
const functionNodes = []
|
|
2909
|
+
const tabById = new Map()
|
|
2910
|
+
|
|
2911
|
+
RED.nodes.eachNode((n) => {
|
|
2912
|
+
if (!n || typeof n !== 'object') return
|
|
2913
|
+
if (String(n.type || '') === 'tab') {
|
|
2914
|
+
tabById.set(String(n.id || ''), String(n.label || n.name || ''))
|
|
2915
|
+
}
|
|
2916
|
+
})
|
|
2917
|
+
|
|
2918
|
+
RED.nodes.eachNode((n) => {
|
|
2919
|
+
if (!n || typeof n !== 'object') return
|
|
2920
|
+
if (String(n.type || '') !== 'function') return
|
|
2921
|
+
|
|
2922
|
+
const func = normalizeCodeBlockText(n.func)
|
|
2923
|
+
const initialize = normalizeCodeBlockText(n.initialize)
|
|
2924
|
+
const finalize = normalizeCodeBlockText(n.finalize)
|
|
2925
|
+
if (!func && !initialize && !finalize) return
|
|
2926
|
+
|
|
2927
|
+
const gaRefs = new Set()
|
|
2928
|
+
extractGAsFromValue({ value: n, outSet: gaRefs, gaRe, maxItems: 24 })
|
|
2929
|
+
|
|
2930
|
+
functionNodes.push({
|
|
2931
|
+
id: String(n.id || ''),
|
|
2932
|
+
name: String(n.name || ''),
|
|
2933
|
+
tabLabel: tabById.get(String(n.z || '')) || '',
|
|
2934
|
+
outputs: Number.isFinite(Number(n.outputs)) ? Number(n.outputs) : '',
|
|
2935
|
+
libs: Array.isArray(n.libs) ? n.libs : [],
|
|
2936
|
+
gaRefs: Array.from(gaRefs.values()).slice(0, 24),
|
|
2937
|
+
func,
|
|
2938
|
+
initialize,
|
|
2939
|
+
finalize
|
|
2940
|
+
})
|
|
2941
|
+
})
|
|
2942
|
+
|
|
2943
|
+
if (!functionNodes.length) return ''
|
|
2944
|
+
|
|
2945
|
+
const shorten = (id) => (id && id.length > 8) ? id.slice(0, 8) : id
|
|
2946
|
+
const safeLine = (s) => String(s || '').replace(/\s+/g, ' ').trim()
|
|
2947
|
+
const limit = Math.max(1200, Number(maxChars) || 0)
|
|
2948
|
+
const nodeLimit = Math.max(1, Number(maxNodes) || 1)
|
|
2949
|
+
const lines = [
|
|
2950
|
+
'Node-RED Function node source code:',
|
|
2951
|
+
'The following JavaScript comes from the live Node-RED flow. Review it directly. If any block is truncated, say that explicitly.'
|
|
2952
|
+
]
|
|
2953
|
+
|
|
2954
|
+
let totalChars = lines.join('\n').length
|
|
2955
|
+
let included = 0
|
|
2956
|
+
let truncatedBlocks = 0
|
|
2957
|
+
|
|
2958
|
+
const sortedNodes = functionNodes
|
|
2959
|
+
.sort((a, b) => {
|
|
2960
|
+
const at = (a.tabLabel || '').localeCompare(b.tabLabel || '')
|
|
2961
|
+
if (at !== 0) return at
|
|
2962
|
+
const an = (a.name || a.id).localeCompare(b.name || b.id)
|
|
2963
|
+
if (an !== 0) return an
|
|
2964
|
+
return (a.id || '').localeCompare(b.id || '')
|
|
2965
|
+
})
|
|
2966
|
+
.slice(0, nodeLimit)
|
|
2967
|
+
|
|
2968
|
+
const buildSection = (label, code, remainingChars) => {
|
|
2969
|
+
const normalized = normalizeCodeBlockText(code)
|
|
2970
|
+
if (!normalized) return ''
|
|
2971
|
+
const overhead = `${label}:\n\`\`\`javascript\n\n\`\`\``.length
|
|
2972
|
+
const availableCodeChars = Math.max(120, remainingChars - overhead)
|
|
2973
|
+
const truncated = normalized.length > availableCodeChars
|
|
2974
|
+
const finalCode = truncated ? truncatePromptText(normalized, availableCodeChars) : normalized
|
|
2975
|
+
return {
|
|
2976
|
+
text: `${label}:\n\`\`\`javascript\n${finalCode}\n\`\`\``,
|
|
2977
|
+
truncated
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
for (const item of sortedNodes) {
|
|
2982
|
+
if (included >= nodeLimit) break
|
|
2983
|
+
const remainingBeforeHeader = limit - totalChars
|
|
2984
|
+
if (remainingBeforeHeader < 220) break
|
|
2985
|
+
|
|
2986
|
+
const header = []
|
|
2987
|
+
header.push(`Function node ${included + 1}: ${item.name || shorten(item.id) || 'unnamed'}`)
|
|
2988
|
+
if (item.tabLabel) header.push(`tab="${safeLine(item.tabLabel)}"`)
|
|
2989
|
+
header.push(`id=${shorten(item.id)}`)
|
|
2990
|
+
if (item.outputs !== '') header.push(`outputs=${item.outputs}`)
|
|
2991
|
+
if (item.gaRefs.length) header.push(`gaRefs="${safeLine(item.gaRefs.join(','))}"`)
|
|
2992
|
+
if (item.libs.length) {
|
|
2993
|
+
const libsLabel = item.libs
|
|
2994
|
+
.map(lib => safeLine((lib && (lib.module || lib.var)) ? `${lib.var || ''}:${lib.module || ''}` : safeStringify(lib)))
|
|
2995
|
+
.filter(Boolean)
|
|
2996
|
+
.join(', ')
|
|
2997
|
+
if (libsLabel) header.push(`libs="${libsLabel}"`)
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
const blockLines = [header.join(' | ')]
|
|
3001
|
+
let remainingForSections = limit - totalChars - header.join(' | ').length - 2
|
|
3002
|
+
if (remainingForSections < 180) break
|
|
3003
|
+
|
|
3004
|
+
const sections = [
|
|
3005
|
+
buildSection('Main function body', item.func, remainingForSections)
|
|
3006
|
+
]
|
|
3007
|
+
remainingForSections -= sections[0] && sections[0].text ? sections[0].text.length + 1 : 0
|
|
3008
|
+
|
|
3009
|
+
const initSection = buildSection('On Start / initialize', item.initialize, remainingForSections)
|
|
3010
|
+
if (initSection && initSection.text) {
|
|
3011
|
+
sections.push(initSection)
|
|
3012
|
+
remainingForSections -= initSection.text.length + 1
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
const finalizeSection = buildSection('On Stop / finalize', item.finalize, remainingForSections)
|
|
3016
|
+
if (finalizeSection && finalizeSection.text) sections.push(finalizeSection)
|
|
3017
|
+
|
|
3018
|
+
sections.forEach((section) => {
|
|
3019
|
+
if (section && section.truncated) truncatedBlocks += 1
|
|
3020
|
+
if (section && section.text) blockLines.push(section.text)
|
|
3021
|
+
})
|
|
3022
|
+
|
|
3023
|
+
const blockText = blockLines.filter(Boolean).join('\n')
|
|
3024
|
+
if (!blockText.trim()) continue
|
|
3025
|
+
if ((totalChars + blockText.length + 1) > limit && included > 0) break
|
|
3026
|
+
|
|
3027
|
+
lines.push(blockText)
|
|
3028
|
+
totalChars += blockText.length + 1
|
|
3029
|
+
included += 1
|
|
3030
|
+
if (totalChars >= limit) break
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
const omittedCount = Math.max(0, functionNodes.length - included)
|
|
3034
|
+
if (omittedCount > 0) {
|
|
3035
|
+
lines.push(`Additional function nodes omitted due to prompt budget: ${omittedCount}.`)
|
|
3036
|
+
}
|
|
3037
|
+
if (truncatedBlocks > 0) {
|
|
3038
|
+
lines.push(`Truncated code blocks due to prompt budget: ${truncatedBlocks}.`)
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
return lines.join('\n\n').trim()
|
|
3042
|
+
} catch (error) {
|
|
3043
|
+
return ''
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2745
3047
|
if (!adminEndpointsRegistered) {
|
|
2746
3048
|
adminEndpointsRegistered = true
|
|
2747
3049
|
|
|
@@ -3531,6 +3833,8 @@ module.exports = function (RED) {
|
|
|
3531
3833
|
|
|
3532
3834
|
node.analysisWindowSec = Number(config.analysisWindowSec || 60)
|
|
3533
3835
|
node.historyWindowSec = Number(config.historyWindowSec || 300)
|
|
3836
|
+
node.historyStoreToDisk = config.historyStoreToDisk !== undefined ? coerceBoolean(config.historyStoreToDisk) : false
|
|
3837
|
+
node.historyStoreRetentionDays = Math.max(1, Number.isFinite(Number(config.historyStoreRetentionDays)) ? Number(config.historyStoreRetentionDays) : 10)
|
|
3534
3838
|
node.emitIntervalSec = Number(config.emitIntervalSec || 0)
|
|
3535
3839
|
node.topN = Number(config.topN || 10)
|
|
3536
3840
|
|
|
@@ -3561,9 +3865,6 @@ module.exports = function (RED) {
|
|
|
3561
3865
|
node.llmMaxEventsInPrompt = (config.llmMaxEventsInPrompt === undefined || config.llmMaxEventsInPrompt === '') ? 120 : Number(config.llmMaxEventsInPrompt)
|
|
3562
3866
|
node.llmIncludeRaw = config.llmIncludeRaw !== undefined ? coerceBoolean(config.llmIncludeRaw) : false
|
|
3563
3867
|
node.llmIncludeFlowContext = config.llmIncludeFlowContext !== undefined ? coerceBoolean(config.llmIncludeFlowContext) : true
|
|
3564
|
-
node.llmMaxFlowNodesInPrompt = (config.llmMaxFlowNodesInPrompt === undefined || config.llmMaxFlowNodesInPrompt === '')
|
|
3565
|
-
? 400
|
|
3566
|
-
: Number(config.llmMaxFlowNodesInPrompt)
|
|
3567
3868
|
node.llmIncludeDocsSnippets = config.llmIncludeDocsSnippets !== undefined ? coerceBoolean(config.llmIncludeDocsSnippets) : true
|
|
3568
3869
|
node.llmDocsLanguage = config.llmDocsLanguage ? String(config.llmDocsLanguage) : 'it'
|
|
3569
3870
|
node.llmDocsMaxSnippets = (config.llmDocsMaxSnippets === undefined || config.llmDocsMaxSnippets === '') ? 5 : Number(config.llmDocsMaxSnippets)
|
|
@@ -3632,6 +3933,7 @@ module.exports = function (RED) {
|
|
|
3632
3933
|
node._gaRateSeries = new Map()
|
|
3633
3934
|
node._gaLabelCsvCache = { ref: null, map: {} }
|
|
3634
3935
|
node._busConnectionWatchTimer = null
|
|
3936
|
+
node._historyDiskLastPruneAt = 0
|
|
3635
3937
|
node._busConnectionState = (node.serverKNX && typeof node.serverKNX.linkStatus === 'string')
|
|
3636
3938
|
? String(node.serverKNX.linkStatus).toLowerCase()
|
|
3637
3939
|
: 'unknown'
|
|
@@ -4738,8 +5040,10 @@ module.exports = function (RED) {
|
|
|
4738
5040
|
const compactMode = compact === true
|
|
4739
5041
|
const maxEventsRequested = Math.max(10, Number(node.llmMaxEventsInPrompt) || 120)
|
|
4740
5042
|
const maxEvents = Math.min(compactMode ? 80 : 240, maxEventsRequested)
|
|
4741
|
-
const
|
|
5043
|
+
const promptEvents = selectTelegramsForPrompt({ question, maxEvents })
|
|
5044
|
+
const recent = Array.isArray(promptEvents.events) ? promptEvents.events : []
|
|
4742
5045
|
const wantsSvgChart = shouldGenerateSvgChart(question)
|
|
5046
|
+
const wantsFunctionNodeSourceContext = node.llmIncludeFlowContext && shouldIncludeFunctionNodeSourceContext(question)
|
|
4743
5047
|
const areasSnapshot = buildAreasSnapshot({ summary })
|
|
4744
5048
|
const areasContext = buildAreasPromptContext(areasSnapshot)
|
|
4745
5049
|
const summaryForPrompt = buildLlmSummarySnapshot(summary)
|
|
@@ -4751,6 +5055,7 @@ module.exports = function (RED) {
|
|
|
4751
5055
|
return `${new Date(t.ts).toISOString()} ${t.event} ${t.source} -> ${t.destination}${devName} dpt=${t.dpt} payload=${payloadStr}${rawStr}`
|
|
4752
5056
|
})
|
|
4753
5057
|
const recentLines = takeLastItemsByCharBudget(lines, compactMode ? 2600 : 7000)
|
|
5058
|
+
const archiveScopeLine = `Prompt event source: ${promptEvents.source}. Time range: ${promptEvents.range && promptEvents.range.label ? promptEvents.range.label : 'recent events'}. Events selected: ${recent.length}.`
|
|
4754
5059
|
|
|
4755
5060
|
let flowContext = ''
|
|
4756
5061
|
if (node.llmIncludeFlowContext) {
|
|
@@ -4760,17 +5065,38 @@ module.exports = function (RED) {
|
|
|
4760
5065
|
if (node._flowContextCache && node._flowContextCache.text && (now - (node._flowContextCache.at || 0)) < ttlMs) {
|
|
4761
5066
|
flowContext = node._flowContextCache.text
|
|
4762
5067
|
} else {
|
|
4763
|
-
|
|
4764
|
-
const maxFlowNodes = compactMode
|
|
4765
|
-
? (configuredMaxFlowNodes > 0 ? Math.min(configuredMaxFlowNodes, 30) : 0)
|
|
4766
|
-
: configuredMaxFlowNodes
|
|
4767
|
-
flowContext = buildKnxUltimateFlowInventory({ maxNodes: maxFlowNodes })
|
|
5068
|
+
flowContext = buildKnxUltimateProjectInventory()
|
|
4768
5069
|
flowContext = truncatePromptText(flowContext, flowMaxChars)
|
|
4769
5070
|
node._flowContextCache = { at: now, text: flowContext }
|
|
4770
5071
|
}
|
|
4771
5072
|
flowContext = truncatePromptText(flowContext, flowMaxChars)
|
|
4772
5073
|
}
|
|
4773
5074
|
|
|
5075
|
+
let functionNodeSourceContext = ''
|
|
5076
|
+
if (wantsFunctionNodeSourceContext) {
|
|
5077
|
+
const sourceMaxChars = compactMode ? 4500 : 18000
|
|
5078
|
+
const sourceMaxNodes = compactMode ? 4 : 12
|
|
5079
|
+
const ttlMs = 10 * 1000
|
|
5080
|
+
const now = nowMs()
|
|
5081
|
+
if (
|
|
5082
|
+
node._functionNodeSourceContextCache &&
|
|
5083
|
+
node._functionNodeSourceContextCache.text &&
|
|
5084
|
+
node._functionNodeSourceContextCache.maxChars === sourceMaxChars &&
|
|
5085
|
+
node._functionNodeSourceContextCache.maxNodes === sourceMaxNodes &&
|
|
5086
|
+
(now - (node._functionNodeSourceContextCache.at || 0)) < ttlMs
|
|
5087
|
+
) {
|
|
5088
|
+
functionNodeSourceContext = node._functionNodeSourceContextCache.text
|
|
5089
|
+
} else {
|
|
5090
|
+
functionNodeSourceContext = buildFunctionNodeSourceContext({ maxChars: sourceMaxChars, maxNodes: sourceMaxNodes })
|
|
5091
|
+
node._functionNodeSourceContextCache = {
|
|
5092
|
+
at: now,
|
|
5093
|
+
maxChars: sourceMaxChars,
|
|
5094
|
+
maxNodes: sourceMaxNodes,
|
|
5095
|
+
text: functionNodeSourceContext
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
|
|
4774
5100
|
let docsContext = ''
|
|
4775
5101
|
if (node.llmIncludeDocsSnippets) {
|
|
4776
5102
|
const docsMaxCharsConfigured = Math.max(500, Math.min(5000, Number(node.llmDocsMaxChars) || 500))
|
|
@@ -4804,6 +5130,8 @@ module.exports = function (RED) {
|
|
|
4804
5130
|
flowContext ? 'Node-RED context:' : '',
|
|
4805
5131
|
flowContext || '',
|
|
4806
5132
|
flowContext ? '' : '',
|
|
5133
|
+
functionNodeSourceContext || '',
|
|
5134
|
+
functionNodeSourceContext ? '' : '',
|
|
4807
5135
|
docsContext || '',
|
|
4808
5136
|
docsContext ? '' : '',
|
|
4809
5137
|
wantsSvgChart ? 'SVG output rules:' : '',
|
|
@@ -4812,7 +5140,9 @@ module.exports = function (RED) {
|
|
|
4812
5140
|
wantsSvgChart ? '- Do not use JavaScript, external URLs, or <foreignObject>.' : '',
|
|
4813
5141
|
wantsSvgChart ? '- Prefer width via viewBox and include labels + legend when useful.' : '',
|
|
4814
5142
|
wantsSvgChart ? '' : '',
|
|
4815
|
-
|
|
5143
|
+
archiveScopeLine,
|
|
5144
|
+
'',
|
|
5145
|
+
'Selected KNX telegrams:',
|
|
4816
5146
|
recentLines.join('\n'),
|
|
4817
5147
|
'',
|
|
4818
5148
|
'User request:',
|
|
@@ -4859,6 +5189,172 @@ module.exports = function (RED) {
|
|
|
4859
5189
|
return path.join(baseDir, 'knxai', 'config', `knxai-config-${node.id}.json`)
|
|
4860
5190
|
}
|
|
4861
5191
|
|
|
5192
|
+
const getHistoryArchiveDir = () => {
|
|
5193
|
+
const baseDir = (node.serverKNX && node.serverKNX.userDir)
|
|
5194
|
+
? node.serverKNX.userDir
|
|
5195
|
+
: path.join(RED.settings.userDir, 'knxultimatestorage')
|
|
5196
|
+
return path.join(baseDir, 'knxai', 'history', node.id)
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
const getHistoryArchiveFile = (dayKey) => path.join(getHistoryArchiveDir(), `${String(dayKey || '').trim() || formatArchiveDayKey(Date.now())}.jsonl`)
|
|
5200
|
+
|
|
5201
|
+
const pruneHistoryArchiveFiles = ({ force = false } = {}) => {
|
|
5202
|
+
if (node.historyStoreToDisk !== true) return
|
|
5203
|
+
const retentionDays = Math.max(1, Math.round(Number.isFinite(Number(node.historyStoreRetentionDays)) ? Number(node.historyStoreRetentionDays) : 1))
|
|
5204
|
+
const now = nowMs()
|
|
5205
|
+
if (!force && (now - Number(node._historyDiskLastPruneAt || 0)) < (60 * 60 * 1000)) return
|
|
5206
|
+
node._historyDiskLastPruneAt = now
|
|
5207
|
+
const dirPath = getHistoryArchiveDir()
|
|
5208
|
+
try {
|
|
5209
|
+
if (!fs.existsSync(dirPath)) return
|
|
5210
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
5211
|
+
const cutoffTs = now - ((retentionDays - 1) * 24 * 60 * 60 * 1000)
|
|
5212
|
+
const cutoffDayKey = formatArchiveDayKey(cutoffTs)
|
|
5213
|
+
for (let i = 0; i < entries.length; i++) {
|
|
5214
|
+
const entry = entries[i]
|
|
5215
|
+
if (!entry || !entry.isFile()) continue
|
|
5216
|
+
const match = String(entry.name || '').match(/^(\d{4}-\d{2}-\d{2})\.jsonl$/)
|
|
5217
|
+
if (!match) continue
|
|
5218
|
+
const dayKey = match[1]
|
|
5219
|
+
if (dayKey < cutoffDayKey) {
|
|
5220
|
+
try { fs.unlinkSync(path.join(dirPath, entry.name)) } catch (error) { /* ignore */ }
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
} catch (error) {
|
|
5224
|
+
node.sysLogger?.warn(`KNX AI history prune error: ${error.message || error}`)
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
const persistTelegramToDisk = (telegram) => {
|
|
5229
|
+
if (node.historyStoreToDisk !== true || !telegram || typeof telegram !== 'object') return
|
|
5230
|
+
const archiveDir = getHistoryArchiveDir()
|
|
5231
|
+
if (!ensureDirectorySync(archiveDir)) return
|
|
5232
|
+
const dayKey = formatArchiveDayKey(telegram.ts || Date.now())
|
|
5233
|
+
const filePath = getHistoryArchiveFile(dayKey)
|
|
5234
|
+
const line = JSON.stringify(telegram) + '\n'
|
|
5235
|
+
fs.appendFile(filePath, line, 'utf8', (error) => {
|
|
5236
|
+
if (error) node.sysLogger?.warn(`KNX AI history append error: ${error.message || error}`)
|
|
5237
|
+
})
|
|
5238
|
+
pruneHistoryArchiveFiles()
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5241
|
+
const loadRecentHistoryFromDisk = () => {
|
|
5242
|
+
if (node.historyStoreToDisk !== true) return
|
|
5243
|
+
const archiveDir = getHistoryArchiveDir()
|
|
5244
|
+
try {
|
|
5245
|
+
if (!fs.existsSync(archiveDir)) return
|
|
5246
|
+
const now = nowMs()
|
|
5247
|
+
const cutoffTs = now - (Math.max(5, Number(node.historyWindowSec || 5)) * 1000)
|
|
5248
|
+
const dayKeys = collectArchiveDayKeysBetween({ fromTs: cutoffTs, toTs: now })
|
|
5249
|
+
if (!dayKeys.length) return
|
|
5250
|
+
const restored = []
|
|
5251
|
+
for (let i = 0; i < dayKeys.length; i++) {
|
|
5252
|
+
const filePath = getHistoryArchiveFile(dayKeys[i])
|
|
5253
|
+
if (!fs.existsSync(filePath)) continue
|
|
5254
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
5255
|
+
if (!raw || String(raw).trim() === '') continue
|
|
5256
|
+
const lines = raw.split(/\r?\n/)
|
|
5257
|
+
for (let j = 0; j < lines.length; j++) {
|
|
5258
|
+
const line = lines[j]
|
|
5259
|
+
if (!line) continue
|
|
5260
|
+
try {
|
|
5261
|
+
const telegram = JSON.parse(line)
|
|
5262
|
+
const ts = Number(telegram && telegram.ts ? telegram.ts : 0)
|
|
5263
|
+
if (!Number.isFinite(ts) || ts < cutoffTs || ts > now) continue
|
|
5264
|
+
restored.push(telegram)
|
|
5265
|
+
} catch (error) {
|
|
5266
|
+
// Ignore malformed archive rows.
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
if (!restored.length) return
|
|
5271
|
+
restored.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))
|
|
5272
|
+
node._history = restored
|
|
5273
|
+
trimHistory(now)
|
|
5274
|
+
} catch (error) {
|
|
5275
|
+
node.sysLogger?.warn(`KNX AI history restore error: ${error.message || error}`)
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
const loadHistorySliceFromDisk = ({ fromTs, toTs, limit = 240 } = {}) => {
|
|
5280
|
+
if (node.historyStoreToDisk !== true) return []
|
|
5281
|
+
const archiveDir = getHistoryArchiveDir()
|
|
5282
|
+
try {
|
|
5283
|
+
if (!fs.existsSync(archiveDir)) return []
|
|
5284
|
+
const from = Number(fromTs || 0)
|
|
5285
|
+
const to = Number(toTs || 0)
|
|
5286
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return []
|
|
5287
|
+
const dayKeys = collectArchiveDayKeysBetween({ fromTs: from, toTs: to })
|
|
5288
|
+
if (!dayKeys.length) return []
|
|
5289
|
+
const items = []
|
|
5290
|
+
for (let i = 0; i < dayKeys.length; i++) {
|
|
5291
|
+
const filePath = getHistoryArchiveFile(dayKeys[i])
|
|
5292
|
+
if (!fs.existsSync(filePath)) continue
|
|
5293
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
5294
|
+
if (!raw || String(raw).trim() === '') continue
|
|
5295
|
+
const lines = raw.split(/\r?\n/)
|
|
5296
|
+
for (let j = 0; j < lines.length; j++) {
|
|
5297
|
+
const line = lines[j]
|
|
5298
|
+
if (!line) continue
|
|
5299
|
+
try {
|
|
5300
|
+
const telegram = JSON.parse(line)
|
|
5301
|
+
const ts = Number(telegram && telegram.ts ? telegram.ts : 0)
|
|
5302
|
+
if (!Number.isFinite(ts) || ts < from || ts > to) continue
|
|
5303
|
+
items.push(telegram)
|
|
5304
|
+
} catch (error) {
|
|
5305
|
+
// Ignore malformed archive rows.
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
if (!items.length) return []
|
|
5310
|
+
items.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))
|
|
5311
|
+
return items.slice(-Math.max(1, Number(limit || 1)))
|
|
5312
|
+
} catch (error) {
|
|
5313
|
+
node.sysLogger?.warn(`KNX AI history load slice error: ${error.message || error}`)
|
|
5314
|
+
return []
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5318
|
+
const selectTelegramsForPrompt = ({ question, maxEvents }) => {
|
|
5319
|
+
const now = nowMs()
|
|
5320
|
+
const maxItems = Math.max(10, Number(maxEvents) || 120)
|
|
5321
|
+
const explicitRange = parseQuestionTimeRange(question, now)
|
|
5322
|
+
const fallbackRange = node.historyStoreToDisk === true
|
|
5323
|
+
? { fromTs: now - (24 * 60 * 60 * 1000), toTs: now, label: 'last 24 hours', explicit: false }
|
|
5324
|
+
: { fromTs: now - (Math.max(5, Number(node.historyWindowSec || 5)) * 1000), toTs: now, label: 'memory window', explicit: false }
|
|
5325
|
+
const range = explicitRange || fallbackRange
|
|
5326
|
+
|
|
5327
|
+
let selected = []
|
|
5328
|
+
let source = 'memory'
|
|
5329
|
+
if (node.historyStoreToDisk === true) {
|
|
5330
|
+
const diskItems = loadHistorySliceFromDisk({ fromTs: range.fromTs, toTs: range.toTs, limit: maxItems * 3 })
|
|
5331
|
+
const memoryItems = node._history.filter(t => Number(t && t.ts ? t.ts : 0) >= range.fromTs && Number(t && t.ts ? t.ts : 0) <= range.toTs)
|
|
5332
|
+
const dedupe = new Map()
|
|
5333
|
+
diskItems.concat(memoryItems).forEach((telegram) => {
|
|
5334
|
+
if (!telegram || typeof telegram !== 'object') return
|
|
5335
|
+
const key = [
|
|
5336
|
+
Number(telegram.ts || 0),
|
|
5337
|
+
String(telegram.event || ''),
|
|
5338
|
+
String(telegram.source || ''),
|
|
5339
|
+
String(telegram.destination || ''),
|
|
5340
|
+
normalizeValueForCompare(telegram.payload),
|
|
5341
|
+
String(telegram.rawHex || '')
|
|
5342
|
+
].join('|')
|
|
5343
|
+
dedupe.set(key, telegram)
|
|
5344
|
+
})
|
|
5345
|
+
selected = Array.from(dedupe.values()).sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0)).slice(-maxItems)
|
|
5346
|
+
source = 'archive+memory'
|
|
5347
|
+
} else {
|
|
5348
|
+
selected = node._history.slice(-maxItems)
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
return {
|
|
5352
|
+
events: selected,
|
|
5353
|
+
source,
|
|
5354
|
+
range
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
|
|
4862
5358
|
const loadPersistedAiConfig = () => {
|
|
4863
5359
|
if (node._persistedAiConfigCache && typeof node._persistedAiConfigCache === 'object') return node._persistedAiConfigCache
|
|
4864
5360
|
const configPath = getAiConfigStorageFile()
|
|
@@ -7068,6 +7564,7 @@ module.exports = function (RED) {
|
|
|
7068
7564
|
const telegram = extractTelegram(msg)
|
|
7069
7565
|
if (!telegram) return
|
|
7070
7566
|
node._history.push(telegram)
|
|
7567
|
+
persistTelegramToDisk(telegram)
|
|
7071
7568
|
resolveTelegramWaiters(telegram)
|
|
7072
7569
|
trackTransitionTelemetry(telegram)
|
|
7073
7570
|
const now = telegram.ts
|
|
@@ -7319,6 +7816,13 @@ module.exports = function (RED) {
|
|
|
7319
7816
|
}, Math.max(5, node.emitIntervalSec) * 1000)
|
|
7320
7817
|
}
|
|
7321
7818
|
|
|
7819
|
+
try {
|
|
7820
|
+
pruneHistoryArchiveFiles({ force: true })
|
|
7821
|
+
loadRecentHistoryFromDisk()
|
|
7822
|
+
} catch (error) {
|
|
7823
|
+
node.sysLogger?.warn(`KNX AI history startup error: ${error.message || error}`)
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7322
7826
|
if (node._busConnectionWatchTimer) clearInterval(node._busConnectionWatchTimer)
|
|
7323
7827
|
node._busConnectionWatchTimer = setInterval(() => {
|
|
7324
7828
|
pollBusConnectionStatus()
|