iobroker.javascript 10.3.0 → 10.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +23 -22
  2. package/admin/assets/AiChatPanel-CfjSsVpE.js +27 -0
  3. package/admin/assets/AiChatService-rFZwYNPk.js +216 -0
  4. package/admin/assets/AiDiffView-DeynDw7L.js +1 -0
  5. package/admin/assets/AiInlineProvider-R_AVYwFT.js +5 -0
  6. package/admin/assets/{BlocklyEditor-BZyoN1JE.js → BlocklyEditor-DPJkoxjP.js} +7 -7
  7. package/admin/assets/Compile-BgTrxmPA.js +165 -0
  8. package/admin/assets/Debugger-CThCzBvP.js +15 -0
  9. package/admin/assets/Diff--plofAtP.js +1 -0
  10. package/admin/assets/{Documentation-uz4Ut9pT.js → Documentation-D3E0U4AK.js} +4 -4
  11. package/admin/assets/ExpandMore-Dio4GeHC.js +1 -0
  12. package/admin/assets/{FbEditor-aOZ-RpPV.js → FbEditor-A7I5U0hA.js} +6 -6
  13. package/admin/assets/History-Ewi5YEEn.js +1 -0
  14. package/admin/assets/Import-6Mm2vrM2.js +1 -0
  15. package/admin/assets/MonacoDiff-BYvG7IqB.js +1 -0
  16. package/admin/assets/{NotificationsActive-BdzH44gp.js → NotificationsActive-CuNfWVJM.js} +1 -1
  17. package/admin/assets/RulesEditor-MDkGXERp.js +1 -0
  18. package/admin/assets/{ScriptEditor-CviB6Hif.js → ScriptEditor-CqphehA8.js} +2 -2
  19. package/admin/assets/ScriptEditorVanillaMonaco-CxgNa-3O.js +3 -0
  20. package/admin/assets/Send-Pz1eveU5.js +1 -0
  21. package/admin/assets/_virtual_mf-localSharedImportMap___mfe_internal__iobroker_javascript__mf_owner__1-Ceh8EDcF.js +1 -0
  22. package/admin/assets/_virtual_mf___mfe_internal__iobroker_javascript__mf_owner__234968779812303__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BRdHvuuG.js +1217 -0
  23. package/admin/assets/_virtual_mf___mfe_internal__iobroker_javascript__mf_owner__234968779812303__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-DLWT3PGo.js +9 -0
  24. package/admin/assets/aiPromptBuilder-CI2ChET0.js +1 -0
  25. package/admin/assets/{blockly-fcfyKYzp.js → blockly-B0t_IRJQ.js} +1 -1
  26. package/admin/assets/{blocklyTheme-CGhU3cEO.js → blocklyTheme-iZoge1S9.js} +1 -1
  27. package/admin/assets/{blocks_action-BQ4O7NRE.js → blocks_action-3CepYoiX.js} +1 -1
  28. package/admin/assets/{blocks_convert-0VBjfhqQ.js → blocks_convert-BEpxVx-_.js} +1 -1
  29. package/admin/assets/{blocks_logic-DAo99xDr.js → blocks_logic-CyIBT53p.js} +1 -1
  30. package/admin/assets/{blocks_number-C--e19NU.js → blocks_number-yqm4qwfh.js} +1 -1
  31. package/admin/assets/{blocks_object-D0TPhdzg.js → blocks_object-CizLkmWp.js} +1 -1
  32. package/admin/assets/blocks_procedures-pFTThYqz.js +3 -0
  33. package/admin/assets/{blocks_sendto-CRIrtOUc.js → blocks_sendto-rEq1xPmO.js} +2 -2
  34. package/admin/assets/{blocks_switch-b_GYfF0n.js → blocks_switch-D3aybjqu.js} +1 -1
  35. package/admin/assets/{blocks_system-BdkUdSsc.js → blocks_system-BVRsS2OE.js} +1 -1
  36. package/admin/assets/{blocks_text-DSFhEVOf.js → blocks_text-D93xQ_03.js} +1 -1
  37. package/admin/assets/{blocks_time-CqALAiJp.js → blocks_time-ChxhjgD7.js} +1 -1
  38. package/admin/assets/{blocks_timeout-yrOD2pZp.js → blocks_timeout-CUZ80HaP.js} +1 -1
  39. package/admin/assets/{blocks_trigger-DeAYXpSS.js → blocks_trigger-DivLpAuy.js} +2 -2
  40. package/admin/assets/{cronHoverProvider-DoO2_tpG.js → cronHoverProvider-Curcjz4m.js} +1 -1
  41. package/admin/assets/dist-CzitsFi7.js +1 -0
  42. package/admin/assets/field_cron-RgLY53_S.js +1 -0
  43. package/admin/assets/field_oid-rBhotXJe.js +1 -0
  44. package/admin/assets/field_script-CU7g3-oB.js +1 -0
  45. package/admin/assets/helpers-BZgjFQNf.js +1 -0
  46. package/admin/assets/{helpers-BTGaTdnT.js → helpers-Bt4VKZBw.js} +1 -1
  47. package/admin/assets/hostInit-CrHjJGJJ.js +1 -0
  48. package/admin/assets/index-DzAQnPTD.js +157 -0
  49. package/admin/assets/{inlineChatWidget-BwQVCPkx.js → inlineChatWidget-ChchJD9a.js} +1 -1
  50. package/admin/assets/{inlineDiffController-BsDDsDHn.js → inlineDiffController-ly6qX4jk.js} +1 -1
  51. package/admin/assets/{loadBlocks-RpqM7PwO.js → loadBlocks-DDLPEDa6.js} +23 -23
  52. package/admin/assets/{mf-entry-bootstrap-0-0deef484.js → mf-entry-bootstrap-0-d055f05d.js} +3 -3
  53. package/admin/assets/pendingShares-mBGPbRwZ.js +2 -0
  54. package/admin/assets/{reactour.esm-DX9ZgYlx.js → reactour.esm-Dk3aGxKn.js} +5 -5
  55. package/admin/assets/{stateHoverProvider-BEadnSXp.js → stateHoverProvider-wAsP-THx.js} +1 -1
  56. package/admin/assets/utils-DUKzLJUz.js +4 -0
  57. package/admin/assets/vite-preload-helper-DjNwdemH.js +1 -0
  58. package/admin/custom/assets/Components-fDcxCyN4.js +1 -0
  59. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomJavascriptSet__mf_owner__1-BraOrMSu.js +1 -0
  60. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-DbKT6Hsb.js +1 -0
  61. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-CVvbIPAy.js +1 -0
  62. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-DVb0ZZ1Q.js → _virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BhZYie9-.js} +89 -89
  63. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare___mf_0_mui_mf_1_x_mf_2_date_mf_2_pickers__loadShare__.js-CV50dgj4.js +1 -0
  64. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare__date_mf_2_fns__loadShare__.js-Djg9OMcQ.js +1 -0
  65. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare__leaflet__loadShare__.js-D9EyCd42.js +1 -0
  66. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare__shared-Hb6V6BnJ.js +2 -0
  67. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-BrY-nF2a.js +122 -0
  68. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__prebuild__leaflet__prebuild__-CHjWtrkp.js +1 -0
  69. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__prebuild__react_mf_2_dom_mf_1_client__prebuild__-DOkC-g0X.js +9 -0
  70. package/admin/custom/assets/dist-CzitsFi7.js +1 -0
  71. package/admin/custom/assets/helpers-BZgjFQNf.js +1 -0
  72. package/admin/custom/assets/hostInit-akwaQLq8.js +1 -0
  73. package/admin/custom/assets/index-BkItz-nL.js +1 -0
  74. package/admin/custom/assets/{lib-BmDlTzq2.js → lib-CADOx2YN.js} +1 -1
  75. package/admin/custom/assets/{mf-entry-bootstrap-0-ad1cd7d0.js → mf-entry-bootstrap-0-3c4565e0.js} +3 -3
  76. package/admin/custom/assets/pendingShares-TCMDgBLn.js +2 -0
  77. package/admin/custom/assets/utils-DUKzLJUz.js +4 -0
  78. package/admin/custom/assets/vite-preload-helper-DjNwdemH.js +1 -0
  79. package/admin/custom/customComponents.js +2 -2
  80. package/admin/custom/i18n/de.json +19 -1
  81. package/admin/custom/i18n/en.json +19 -1
  82. package/admin/custom/i18n/es.json +19 -1
  83. package/admin/custom/i18n/fr.json +19 -1
  84. package/admin/custom/i18n/it.json +19 -1
  85. package/admin/custom/i18n/nl.json +19 -1
  86. package/admin/custom/i18n/pl.json +19 -1
  87. package/admin/custom/i18n/pt.json +19 -1
  88. package/admin/custom/i18n/ru.json +19 -1
  89. package/admin/custom/i18n/uk.json +19 -1
  90. package/admin/custom/i18n/zh-cn.json +19 -1
  91. package/admin/i18n/de.json +13 -1
  92. package/admin/i18n/en.json +13 -1
  93. package/admin/i18n/es.json +13 -1
  94. package/admin/i18n/fr.json +13 -1
  95. package/admin/i18n/it.json +13 -1
  96. package/admin/i18n/nl.json +13 -1
  97. package/admin/i18n/pl.json +13 -1
  98. package/admin/i18n/pt.json +13 -1
  99. package/admin/i18n/ru.json +13 -1
  100. package/admin/i18n/uk.json +13 -1
  101. package/admin/i18n/zh-cn.json +13 -1
  102. package/admin/jsonConfig.json +274 -86
  103. package/admin/mf-manifest.json +1 -1
  104. package/admin/mf-stats.json +1 -1
  105. package/admin/remoteEntry.js +2 -2
  106. package/admin/tab.html +2 -2
  107. package/build/lib/ai.js +53 -0
  108. package/build/lib/ai.js.map +1 -0
  109. package/build/lib/inspect.js +66 -10
  110. package/build/lib/inspect.js.map +1 -1
  111. package/build/lib/mirror.js +24 -23
  112. package/build/lib/mirror.js.map +1 -1
  113. package/build/lib/scriptHistory.js +245 -0
  114. package/build/lib/scriptHistory.js.map +1 -0
  115. package/build/main.js +358 -413
  116. package/build/main.js.map +1 -1
  117. package/build/types.d.ts +11 -0
  118. package/io-package.json +17 -15
  119. package/package.json +8 -7
  120. package/admin/assets/AiChatPanel-CpY4Bun-.js +0 -27
  121. package/admin/assets/AiChatService-QKcpW4jQ.js +0 -216
  122. package/admin/assets/AiDiffView-DscPfZ38.js +0 -1
  123. package/admin/assets/AiInlineProvider-DTSPKoXG.js +0 -5
  124. package/admin/assets/Compile-Oss28m_f.js +0 -165
  125. package/admin/assets/Debugger-teJTIU-2.js +0 -15
  126. package/admin/assets/ExpandMore-V5Ix1QB1.js +0 -1
  127. package/admin/assets/Import-BIC5Ee9z.js +0 -1
  128. package/admin/assets/RulesEditor-CcDVthPa.js +0 -1
  129. package/admin/assets/ScriptEditorVanillaMonaco-ByuXwybM.js +0 -3
  130. package/admin/assets/Send-Mdo32jMb.js +0 -1
  131. package/admin/assets/_virtual_mf-localSharedImportMap___mfe_internal__iobroker_javascript__mf_owner__1-BnL93BgK.js +0 -1
  132. package/admin/assets/_virtual_mf___mfe_internal__iobroker_javascript__mf_owner__180366418666083__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-NI-3Uv4e.js +0 -1217
  133. package/admin/assets/_virtual_mf___mfe_internal__iobroker_javascript__mf_owner__180366418666083__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-DVaMIZyC.js +0 -9
  134. package/admin/assets/aiPromptBuilder-yTB6N2VT.js +0 -1
  135. package/admin/assets/blocks_procedures-DgTxgXdB.js +0 -3
  136. package/admin/assets/dist-D4uJqRBO.js +0 -4
  137. package/admin/assets/field_cron-D3Ts_s9M.js +0 -1
  138. package/admin/assets/field_oid-BRNXredg.js +0 -1
  139. package/admin/assets/field_script-3X6ousg7.js +0 -1
  140. package/admin/assets/hostInit-2h2Cbsaw.js +0 -1
  141. package/admin/assets/index-CbQlIKB_.js +0 -156
  142. package/admin/assets/pendingShares-BaAadIDv.js +0 -1
  143. package/admin/assets/vite-preload-helper-CaC3yEL3.js +0 -1
  144. package/admin/custom/assets/Components-FkGDIlId.js +0 -1
  145. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomJavascriptSet__mf_owner__1-Cs88NUCd.js +0 -1
  146. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-P-8ffWXw.js +0 -1
  147. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-ZVuRAEo5.js +0 -1
  148. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-Dna3WF04.js +0 -1
  149. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare___mf_0_mui_mf_1_x_mf_2_date_mf_2_pickers__loadShare__.js-DcjdCGAx.js +0 -1
  150. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare__date_mf_2_fns__loadShare__.js-CnhNjamd.js +0 -1
  151. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare__leaflet__loadShare__.js-Dz7BFJqH.js +0 -1
  152. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-B0olko2J.js +0 -1
  153. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-CzN4reh_.js +0 -122
  154. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__prebuild__leaflet__prebuild__-xPcBaiTQ.js +0 -1
  155. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__prebuild__react_mf_2_dom_mf_1_client__prebuild__-DuFBE5fh.js +0 -9
  156. package/admin/custom/assets/dist-D4uJqRBO.js +0 -4
  157. package/admin/custom/assets/hostInit-6oh4UY2k.js +0 -1
  158. package/admin/custom/assets/index-BrlyEy6J.js +0 -1
  159. package/admin/custom/assets/pendingShares-kV2kwejX.js +0 -1
  160. package/admin/custom/assets/vite-preload-helper-CaC3yEL3.js +0 -1
  161. package/build/lib/aiProviderResolver.js +0 -140
  162. package/build/lib/aiProviderResolver.js.map +0 -1
  163. package/build/lib/anthropicAdapter.js +0 -175
  164. package/build/lib/anthropicAdapter.js.map +0 -1
  165. /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-CXpyx8Vy.js → _virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-CXpyx8Vy.js} +0 -0
package/build/main.js CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * The MIT License (MIT)
6
6
  *
7
- * Copyright (c) 2014-2024 bluefox <dogafox@gmail.com>,
7
+ * Copyright (c) 2014-2026 bluefox <dogafox@gmail.com>,
8
8
  *
9
9
  * Copyright (c) 2014 hobbyquaker
10
10
  */
@@ -80,8 +80,9 @@ const words_1 = require("./lib/words");
80
80
  const sandbox_1 = require("./lib/sandbox");
81
81
  const nodeModulesManagement_1 = require("./lib/nodeModulesManagement");
82
82
  const secrets_1 = require("./lib/secrets");
83
- const aiProviderResolver_1 = require("./lib/aiProviderResolver");
84
- const anthropicAdapter_1 = require("./lib/anthropicAdapter");
83
+ const ai_core_1 = require("@iobroker/ai-core");
84
+ const ai_1 = require("./lib/ai");
85
+ const scriptHistory_1 = require("./lib/scriptHistory");
85
86
  const eventObj_1 = require("./lib/eventObj");
86
87
  const scheduler_1 = require("./lib/scheduler");
87
88
  const typescriptSettings_1 = require("./lib/typescriptSettings");
@@ -251,33 +252,6 @@ const jsDeclarationServer = new lazyTsServer_1.LazyTsServer(typescriptSettings_1
251
252
  * Stores the IDs of script objects whose change should be ignored because
252
253
  * the compiled source was just updated
253
254
  */
254
- const HTTP_STATUS_TEXTS = {
255
- 400: 'Bad Request',
256
- 401: 'Unauthorized',
257
- 403: 'Forbidden',
258
- 404: 'Not Found',
259
- 429: 'Too Many Requests / Rate Limit',
260
- 500: 'Internal Server Error',
261
- 502: 'Bad Gateway',
262
- 503: 'Service Unavailable',
263
- };
264
- function httpStatusText(code) {
265
- return HTTP_STATUS_TEXTS[code] ?? `Error ${code}`;
266
- }
267
- /**
268
- * Resolves the correct http/https module based on the URL string.
269
- * Returns null if the URL is invalid.
270
- */
271
- function resolveRequestModule(url) {
272
- try {
273
- const { protocol } = new URL(url);
274
- const isHttps = protocol === 'https:';
275
- return { module: isHttps ? https : http, isHttps };
276
- }
277
- catch {
278
- return null;
279
- }
280
- }
281
255
  class JavaScript extends adapter_core_1.Adapter {
282
256
  context;
283
257
  errorLogFunction = {
@@ -340,14 +314,11 @@ class JavaScript extends adapter_core_1.Adapter {
340
314
  mirror;
341
315
  stopCounters = {};
342
316
  setStateCountCheckInterval = null;
343
- /**
344
- * Decrypted AI API keys cached from the central credential store (manager mode),
345
- * keyed by credential ID (e.g. `system.credentials.anthropic`). Kept fresh by the
346
- * subscriptions set up in `subscribeAiCredentials`.
347
- */
348
- aiCredentialCache = new Map();
349
- /** Unsubscribe callbacks for the AI credential subscriptions (manager mode). */
350
- aiCredentialUnsubscribers = [];
317
+ /** Providers, keys and the push of long answers for the AI of the script editor */
318
+ ai = new ai_core_1.AiBackend(this, {
319
+ getSettings: () => (0, ai_1.jsAiSettings)(this.config),
320
+ aliases: ai_1.JS_AI_ALIASES,
321
+ });
351
322
  /**
352
323
  * Decrypted credentials of the central ioBroker credential store (`system.credentials.*`),
353
324
  * exposed to the scripts as the global `SECRETS` object. Kept up to date by `onObjectChange`.
@@ -391,6 +362,16 @@ class JavaScript extends adapter_core_1.Adapter {
391
362
  ...options,
392
363
  name: 'javascript', // adapter name
393
364
  useFormatDate: true,
365
+ /*
366
+ * The script editor subscribes here so that a finished AI answer can be *pushed* to it.
367
+ * An AI request regularly runs longer than 30 s, and `@iobroker/ws` answers every socket
368
+ * callback with the string "timeout" after exactly that (socket.io.js: `Date.now() + 30_000`,
369
+ * not configurable). The answer then arrived in a dead callback and the chat stayed empty
370
+ * with nothing in any log. So the editor gets an immediate acknowledgement and the result
371
+ * later as an instance message.
372
+ */
373
+ uiClientSubscribe: (info) => this.ai.onUiClientSubscribe(info) ?? { accepted: false, error: 'Unknown subscription type' },
374
+ uiClientUnsubscribe: (info) => this.ai.onUiClientUnsubscribe(info),
394
375
  /**
395
376
  * If the JS-Controller catches an unhandled error, this will be called,
396
377
  * so we have a chance to handle it ourselves.
@@ -472,6 +453,13 @@ class JavaScript extends adapter_core_1.Adapter {
472
453
  }
473
454
  }
474
455
  }
456
+ if (debugMode) {
457
+ // This process was started by the inspector (see `debugStart`) with `--debug`, so the whole log goes
458
+ // through the pipes of that process. If it is gone, writing to them fails with EPIPE, and an unhandled
459
+ // EPIPE would end the debugged process with an uncaught exception (#2382)
460
+ process.stdout.on('error', () => { });
461
+ process.stderr.on('error', () => { });
462
+ }
475
463
  this.context = {
476
464
  mods: this.mods,
477
465
  objects: this.objects,
@@ -523,6 +511,220 @@ class JavaScript extends adapter_core_1.Adapter {
523
511
  };
524
512
  this.tsServer = new lazyTsServer_1.LazyTsServer(typescriptSettings_1.tsCompilerOptions, this.tsLog, TS_MEMORY_RELEASE_DELAY_MS);
525
513
  }
514
+ /**
515
+ * Make sure the instance has a file area, which is where the script history lives.
516
+ *
517
+ * The adapter's own namespace needs a `meta` object before anything can be written under it.
518
+ * Deliberately not `javascript.admin`: that is the UI's file area and every
519
+ * `iobroker upload javascript` wipes it.
520
+ */
521
+ async ensureHistoryStorage() {
522
+ try {
523
+ await this.setForeignObjectNotExistsAsync(this.namespace, {
524
+ type: 'meta',
525
+ common: {
526
+ name: 'javascript files',
527
+ type: 'meta.user',
528
+ },
529
+ native: {},
530
+ });
531
+ }
532
+ catch (e) {
533
+ this.log.warn(`Cannot create the file storage of the instance: ${e instanceof Error ? e.message : String(e)}`);
534
+ }
535
+ }
536
+ /**
537
+ * Keep the saved source of a script, so it can be looked at and brought back later.
538
+ *
539
+ * Hooked into the object change and not into the editor on purpose: this way a change from the
540
+ * mirror directory, from the object browser, from another script or from the AI is kept just
541
+ * the same as one typed into the editor.
542
+ *
543
+ * @param id id of the script
544
+ * @param obj the script as it is now
545
+ * @param formerObj the script as it was before this change, if it existed
546
+ */
547
+ async storeScriptVersion(id, obj, formerObj) {
548
+ const keep = (0, scriptHistory_1.resolveHistoryVersions)(this.config.historyVersions);
549
+ if (!keep) {
550
+ return;
551
+ }
552
+ // Only the instance that runs the script writes its history, otherwise two instances
553
+ // store every version twice
554
+ if (obj.common?.engine !== `system.adapter.${this.namespace}`) {
555
+ return;
556
+ }
557
+ const source = obj.common.source;
558
+ if (!(0, scriptHistory_1.isWorthStoring)(formerObj?.common?.source, source)) {
559
+ return;
560
+ }
561
+ try {
562
+ const ts = typeof obj.ts === 'number' && obj.ts ? obj.ts : Date.now();
563
+ const entry = {
564
+ ts,
565
+ ...(obj.from ? { from: obj.from } : {}),
566
+ ...(obj.user ? { user: obj.user } : {}),
567
+ // The source of a protected script is encrypted in the object and is kept that
568
+ // way - the history must not be the place where it lies around in plain text
569
+ ...(0, scriptHistory_1.describeSource)(source, !!obj.native?.protected),
570
+ };
571
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, ts), source);
572
+ const raw = await this.readFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id)).catch(() => null);
573
+ const current = (0, scriptHistory_1.parseIndex)((0, scriptHistory_1.fileToString)(raw));
574
+ const { index, obsolete } = (0, scriptHistory_1.addVersion)(current, entry, keep);
575
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id), JSON.stringify(index));
576
+ for (const old of obsolete) {
577
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, old)).catch(() => {
578
+ /* a version that is already gone is one we do not have to delete */
579
+ });
580
+ }
581
+ this.log.debug(`Script history: stored version ${ts} of ${id} (${index.length} kept)`);
582
+ }
583
+ catch (e) {
584
+ // A history is a convenience; it must never get in the way of saving a script
585
+ this.log.warn(`Cannot store the version of ${id}: ${e instanceof Error ? e.message : String(e)}`);
586
+ }
587
+ }
588
+ /**
589
+ * The stored versions of a script, newest first, without their sources.
590
+ *
591
+ * @param id id of the script
592
+ */
593
+ async readScriptVersions(id) {
594
+ try {
595
+ const raw = await this.readFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id));
596
+ return (0, scriptHistory_1.parseIndex)((0, scriptHistory_1.fileToString)(raw));
597
+ }
598
+ catch {
599
+ // No history yet - not an error, just nothing to show
600
+ return [];
601
+ }
602
+ }
603
+ /** What every stored history occupies, biggest first - read from the indexes, never from the sources */
604
+ async collectHistoryUsage() {
605
+ const rows = [];
606
+ let folders = [];
607
+ try {
608
+ folders = await this.readDirAsync(this.namespace, scriptHistory_1.HISTORY_ROOT);
609
+ }
610
+ catch {
611
+ // Nothing stored yet
612
+ }
613
+ for (const folder of folders) {
614
+ const id = (0, scriptHistory_1.decodeFolderName)(folder.file);
615
+ const index = await this.readScriptVersions(id);
616
+ if (!index.length) {
617
+ continue;
618
+ }
619
+ const { versions, bytes } = (0, scriptHistory_1.sumIndex)(index);
620
+ rows.push({ id, exists: !!this.objects[id], versions, bytes });
621
+ }
622
+ rows.sort((a, b) => b.bytes - a.bytes);
623
+ return {
624
+ rows,
625
+ total: {
626
+ scripts: rows.length,
627
+ versions: rows.reduce((sum, r) => sum + r.versions, 0),
628
+ bytes: rows.reduce((sum, r) => sum + r.bytes, 0),
629
+ },
630
+ };
631
+ }
632
+ /**
633
+ * Remove stored versions.
634
+ *
635
+ * @param options what to remove
636
+ * @param options.id the script whose versions are meant
637
+ * @param options.ts one single version of that script; without it the whole history goes
638
+ * @param options.orphans every history whose script no longer exists
639
+ * @param options.all every stored version there is - the scripts themselves stay untouched
640
+ */
641
+ async deleteHistory(options) {
642
+ let deleted = 0;
643
+ let bytes = 0;
644
+ /**
645
+ * Delete versions of one script and write the remaining index back.
646
+ *
647
+ * @param id the script
648
+ * @param keep the versions that stay; an empty list removes the index as well
649
+ * @param drop the versions to remove
650
+ */
651
+ const apply = async (id, keep, drop) => {
652
+ for (const version of drop) {
653
+ bytes += version.size || 0;
654
+ deleted++;
655
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, version.ts)).catch(() => {
656
+ /* a version that is already gone is one we do not have to delete */
657
+ });
658
+ }
659
+ if (keep.length) {
660
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id), JSON.stringify(keep));
661
+ }
662
+ else {
663
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id)).catch(() => {
664
+ /* already gone */
665
+ });
666
+ }
667
+ };
668
+ if (options.orphans || options.all) {
669
+ const { rows } = await this.collectHistoryUsage();
670
+ // `all` takes everything, `orphans` only what has no script behind it anymore
671
+ for (const row of options.all ? rows : rows.filter(r => !r.exists)) {
672
+ await apply(row.id, [], await this.readScriptVersions(row.id));
673
+ }
674
+ }
675
+ else if (options.id) {
676
+ const index = await this.readScriptVersions(options.id);
677
+ if (options.ts) {
678
+ await apply(options.id, index.filter(v => v.ts !== options.ts), index.filter(v => v.ts === options.ts));
679
+ }
680
+ else {
681
+ await apply(options.id, [], index);
682
+ }
683
+ }
684
+ this.log.debug(`Script history: removed ${deleted} versions (${(0, scriptHistory_1.formatBytes)(bytes)})`);
685
+ return { deleted, bytes };
686
+ }
687
+ /**
688
+ * Cut every history down to the configured number of versions.
689
+ *
690
+ * Without this a smaller limit would only take effect script by script, on the next save of
691
+ * each - and setting it to zero would never free anything at all.
692
+ */
693
+ async trimAllHistories() {
694
+ const keep = (0, scriptHistory_1.resolveHistoryVersions)(this.config.historyVersions);
695
+ let folders = [];
696
+ try {
697
+ folders = await this.readDirAsync(this.namespace, scriptHistory_1.HISTORY_ROOT);
698
+ }
699
+ catch {
700
+ return;
701
+ }
702
+ for (const folder of folders) {
703
+ const id = (0, scriptHistory_1.decodeFolderName)(folder.file);
704
+ const index = await this.readScriptVersions(id);
705
+ if (!index.length) {
706
+ continue;
707
+ }
708
+ const { index: kept, obsolete } = (0, scriptHistory_1.trimIndex)(index, keep);
709
+ if (!obsolete.length) {
710
+ continue;
711
+ }
712
+ for (const ts of obsolete) {
713
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, ts)).catch(() => {
714
+ /* already gone */
715
+ });
716
+ }
717
+ if (kept.length) {
718
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id), JSON.stringify(kept));
719
+ }
720
+ else {
721
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id)).catch(() => {
722
+ /* already gone */
723
+ });
724
+ }
725
+ this.log.debug(`Script history: trimmed ${id} to ${kept.length} versions`);
726
+ }
727
+ }
526
728
  async onObjectChange(id, obj) {
527
729
  // Check if we should ignore this change (once!) because we just updated the compiled sources
528
730
  if (this.ignoreObjectChange.has(id)) {
@@ -612,6 +814,10 @@ class JavaScript extends adapter_core_1.Adapter {
612
814
  }
613
815
  }
614
816
  }
817
+ // Keep the saved source before the branches below decide what else to do with it
818
+ if (obj?.type === 'script') {
819
+ await this.storeScriptVersion(id, obj, formerObj);
820
+ }
615
821
  // handle Script object updates
616
822
  if (!obj && formerObj?.type === 'script') {
617
823
  // Object Deleted just now
@@ -821,13 +1027,15 @@ class JavaScript extends adapter_core_1.Adapter {
821
1027
  }
822
1028
  async onUnload(callback) {
823
1029
  try {
824
- await this.debugStop();
1030
+ // Do not wait for it: the inspector needs a moment to stop the debugged process, and it does that by
1031
+ // itself as soon as the connection to this process is gone. The shutdown must not be delayed by it.
1032
+ void this.debugStop();
825
1033
  this.stopTimeSchedules();
826
1034
  if (this.setStateCountCheckInterval) {
827
1035
  clearInterval(this.setStateCountCheckInterval);
828
1036
  this.setStateCountCheckInterval = null;
829
1037
  }
830
- await this.unsubscribeAiCredentials();
1038
+ await this.ai.stop();
831
1039
  this.secretsManager.destroy();
832
1040
  if (this.editorCheckInterval) {
833
1041
  clearInterval(this.editorCheckInterval);
@@ -884,53 +1092,38 @@ class JavaScript extends adapter_core_1.Adapter {
884
1092
  }
885
1093
  await this.main();
886
1094
  }
887
- /** Read and decrypt a single AI credential's key from the central store; returns '' (and logs) on error. */
888
- async readAiCredentialKey(id) {
889
- if (!adapter_core_1.Credentials?.getCredentials) {
890
- this.log.warn(`Cannot read AI credential "${id}": Credentials API is only with 7.2 js-controller available`);
891
- return '';
1095
+ /**
1096
+ * Whether the user a message was sent on behalf of may have code executed here.
1097
+ *
1098
+ * `obj.user` arrives with js-controller 7.2.5 and newer: a socket server such as `admin` or `web`
1099
+ * puts the authenticated user of the connection there, and a socket client cannot set it itself.
1100
+ * Where it is missing - an older controller, a script, an adapter that names nobody - nothing is
1101
+ * checked and everything stays as it was.
1102
+ *
1103
+ * The right asked for is the one `cmdExec` is checked against: running a script here is running
1104
+ * code on this host, whatever the ACL of the single objects it then touches says.
1105
+ *
1106
+ * @param obj the incoming message
1107
+ */
1108
+ async mayExecuteFor(obj) {
1109
+ // `user` is not in the older `@iobroker/types` this adapter builds against
1110
+ const user = obj.user;
1111
+ if (!user) {
1112
+ return null;
892
1113
  }
893
1114
  try {
894
- const cred = await adapter_core_1.Credentials.getCredentials(this, id);
895
- return (cred?.values?.key || '').trim();
1115
+ const permissions = await this.calculatePermissionsAsync(user, {
1116
+ cmdExec: { type: 'other', operation: 'execute' },
1117
+ });
1118
+ if (permissions?.other?.execute) {
1119
+ return null;
1120
+ }
1121
+ return `User "${user}" has no permission to execute scripts`;
896
1122
  }
897
1123
  catch (e) {
898
- this.log.warn(`Cannot read AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
899
- return '';
1124
+ return `Cannot check the permissions of "${user}": ${e instanceof Error ? e.message : String(e)}`;
900
1125
  }
901
1126
  }
902
- /**
903
- * Resolve the API key (and base URL) for an AI provider.
904
- *
905
- * In `manual` mode the key comes from the encryptedNative adapter config.
906
- * In `manager` mode the config only stores the ID of a credential in the central
907
- * ioBroker credential store (`system.credentials.*`); the actual key is taken from the
908
- * `aiCredentialCache` (kept fresh by `subscribeAiCredentials`) or, for credentials we are
909
- * not subscribed to (e.g. a not-yet-saved selection in the settings dialog), read directly.
910
- *
911
- * The settings-dialog Test button may pass form values that are not saved yet
912
- * (`messageApiKey` / `messageCredentialId` / `credentialType`); those win over the stored config.
913
- */
914
- async resolveAiCredentials(provider, opts = {}) {
915
- const mode = opts.credentialType || this.config.credentialType || 'manual';
916
- if (mode === 'manager') {
917
- // The base URL is not a secret and is resolved the same way in both modes.
918
- const { baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
919
- const id = (opts.messageCredentialId || (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider)).trim();
920
- if (!id) {
921
- return { apiKey: '', baseUrl };
922
- }
923
- // Prefer the cached value kept fresh by the credential subscription.
924
- const cached = this.aiCredentialCache.get(id);
925
- const apiKey = cached !== undefined ? cached : await this.readAiCredentialKey(id);
926
- return { apiKey, baseUrl };
927
- }
928
- // Manual mode. The Test button sends the current form key (maybe empty) — let it win.
929
- if (opts.messageApiKey !== undefined) {
930
- return (0, aiProviderResolver_1.resolveTestCredentials)(this.config, provider, opts.messageApiKey, opts.messageBaseUrl);
931
- }
932
- return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
933
- }
934
1127
  /**
935
1128
  * Warns about the one setup that the fix for #2369 stops working.
936
1129
  *
@@ -945,81 +1138,19 @@ class JavaScript extends adapter_core_1.Adapter {
945
1138
  if (!(this.config.gptBaseUrl || '').trim()) {
946
1139
  return;
947
1140
  }
948
- const manager = this.config.credentialType === 'manager';
949
- const custom = manager
950
- ? (0, aiProviderResolver_1.getProviderCredentialId)(this.config, 'custom')
951
- : (this.config.gptBaseUrlKey || '').trim();
952
- const openai = manager ? (0, aiProviderResolver_1.getProviderCredentialId)(this.config, 'openai') : (this.config.gptKey || '').trim();
953
- if (!custom && openai) {
1141
+ const settings = (0, ai_1.jsAiSettings)(this.config);
1142
+ const of = settings.credentialType === 'manager' ? settings.credentialIds : settings.keys;
1143
+ if (!of.custom && of.openai) {
954
1144
  this.log.warn(`A custom AI endpoint ("${this.config.gptBaseUrl}") is configured, but its own API key is empty. ` +
955
1145
  `Requests to it are no longer signed with the OpenAI key - move the key of that endpoint into ` +
956
1146
  `the "Custom API key" field of the adapter settings.`);
957
1147
  }
958
1148
  }
959
- /**
960
- * In `manager` mode, subscribe to all configured AI credentials so that edits made in the
961
- * admin credential manager (Settings → Credentials) are picked up live, without restarting
962
- * the adapter (the `system.credentials.*` objects are global, not part of the instance config).
963
- * The decrypted keys are cached and kept fresh by the subscription handlers.
964
- */
965
- async subscribeAiCredentials() {
966
- // Always start from a clean state (idempotent — also used to re-subscribe).
967
- await this.unsubscribeAiCredentials();
968
- if (this.config.credentialType !== 'manager') {
969
- return;
970
- }
971
- if (!adapter_core_1.Credentials?.subscribeCredentials) {
972
- this.log.warn(`Cannot subscribe AI credential: Credentials API is only with 7.2 js-controller available`);
1149
+ onMessage(obj) {
1150
+ // the AI commands, also under the names the editor used before `ai-core`
1151
+ if (this.ai.handleMessage(obj)) {
973
1152
  return;
974
1153
  }
975
- // Collect the distinct credential IDs configured across all AI providers.
976
- const ids = new Set();
977
- for (const provider of ['openai', 'anthropic', 'gemini', 'deepseek', 'custom']) {
978
- const id = (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider);
979
- if (id) {
980
- ids.add(id);
981
- }
982
- }
983
- for (const id of ids) {
984
- try {
985
- const unsubscribe = await adapter_core_1.Credentials.subscribeCredentials(this, id, (changedId, cred) => {
986
- if (cred) {
987
- this.aiCredentialCache.set(changedId, (cred.values?.key || '').trim());
988
- this.log.debug(`AI credential "${changedId}" updated`);
989
- }
990
- else {
991
- // The credential was deleted
992
- this.aiCredentialCache.delete(changedId);
993
- this.log.debug(`AI credential "${changedId}" was deleted`);
994
- }
995
- });
996
- this.aiCredentialUnsubscribers.push(unsubscribe);
997
- // Prime the cache with the current value (the handler may only fire on later changes).
998
- this.aiCredentialCache.set(id, await this.readAiCredentialKey(id));
999
- }
1000
- catch (e) {
1001
- this.log.warn(`Cannot subscribe to AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
1002
- }
1003
- }
1004
- if (this.aiCredentialUnsubscribers.length) {
1005
- this.log.debug(`Subscribed to ${this.aiCredentialUnsubscribers.length} AI credential(s)`);
1006
- }
1007
- }
1008
- /** Tear down all AI credential subscriptions and clear the cache. */
1009
- async unsubscribeAiCredentials() {
1010
- const unsubscribers = this.aiCredentialUnsubscribers;
1011
- this.aiCredentialUnsubscribers = [];
1012
- this.aiCredentialCache.clear();
1013
- for (const unsubscribe of unsubscribers) {
1014
- try {
1015
- await unsubscribe();
1016
- }
1017
- catch (e) {
1018
- this.log.warn(`Cannot unsubscribe from AI credential: ${e instanceof Error ? e.message : String(e)}`);
1019
- }
1020
- }
1021
- }
1022
- onMessage(obj) {
1023
1154
  switch (obj?.command) {
1024
1155
  // process messageTo commands
1025
1156
  case 'toScript':
@@ -1268,297 +1399,98 @@ class JavaScript extends adapter_core_1.Adapter {
1268
1399
  this.context.rulesOpened = null;
1269
1400
  break;
1270
1401
  }
1271
- case 'getIoBrokerDataDir': {
1272
- if (obj.callback) {
1273
- this.sendTo(obj.from, obj.command, {
1274
- dataDir: (0, adapter_core_1.getAbsoluteDefaultDataDir)(),
1275
- sep: node_path_1.sep,
1276
- }, obj.callback);
1277
- }
1278
- break;
1279
- }
1280
- case 'chatCompletion': {
1281
- // Proxy chat completion requests to an OpenAI-compatible API endpoint.
1282
- // API keys are resolved server-side from the encryptedNative config or the central
1283
- // credentials manager — they never leave the adapter (frontend only sends `provider`).
1402
+ case 'getScriptVersions': {
1403
+ // The stored versions of one script, without their sources - the list must stay
1404
+ // cheap, a history of 30 Blockly versions is megabytes
1284
1405
  void (async () => {
1285
1406
  if (!obj.callback) {
1286
1407
  return;
1287
1408
  }
1288
- const chatModel = (obj.message?.model || '').trim();
1289
- const messages = obj.message?.messages;
1290
- const tools = obj.message?.tools;
1291
- const provider = (obj.message?.provider || 'openai').trim();
1292
- // How long the caller is willing to wait - see `resolveRequestTimeout`
1293
- const requestTimeout = (0, aiProviderResolver_1.resolveRequestTimeout)(obj.message?.timeout);
1294
- const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
1295
- messageBaseUrl: obj.message?.baseUrl,
1296
- });
1297
- // Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
1298
- if (!apiKey &&
1299
- (provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
1300
- this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
1409
+ const scriptId = (obj.message?.id || '').toString();
1410
+ if (!scriptId) {
1411
+ this.sendTo(obj.from, obj.command, { error: 'No script ID provided' }, obj.callback);
1301
1412
  return;
1302
1413
  }
1303
- if (!chatModel || !messages) {
1304
- this.sendTo(obj.from, obj.command, { error: 'Model and messages are required' }, obj.callback);
1414
+ const versions = await this.readScriptVersions(scriptId);
1415
+ this.sendTo(obj.from, obj.command, { versions }, obj.callback);
1416
+ })();
1417
+ break;
1418
+ }
1419
+ case 'getScriptVersion': {
1420
+ // The source of one stored version, fetched when it is actually looked at
1421
+ void (async () => {
1422
+ if (!obj.callback) {
1305
1423
  return;
1306
1424
  }
1307
- let url;
1308
- const chatHeaders = {
1309
- 'Content-Type': 'application/json',
1310
- };
1311
- let bodyObj;
1312
- if (provider === 'anthropic') {
1313
- url = 'https://api.anthropic.com/v1/messages';
1314
- chatHeaders['x-api-key'] = apiKey;
1315
- chatHeaders['anthropic-version'] = '2023-06-01';
1316
- // Translate OpenAI-format messages/tools into Anthropic's content-block format.
1317
- const { system: systemText, messages: anthropicMessages } = (0, anthropicAdapter_1.translateMessagesToAnthropic)(messages);
1318
- const anthropicTools = tools?.length ? (0, anthropicAdapter_1.translateToolsToAnthropic)(tools) : [];
1319
- bodyObj = {
1320
- model: chatModel,
1321
- max_tokens: 8192,
1322
- stream: false,
1323
- ...(systemText ? { system: systemText } : {}),
1324
- messages: anthropicMessages,
1325
- ...(anthropicTools.length ? { tools: anthropicTools } : {}),
1326
- };
1327
- }
1328
- else if (provider === 'gemini') {
1329
- url = 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions';
1330
- if (apiKey) {
1331
- chatHeaders.Authorization = `Bearer ${apiKey}`;
1332
- }
1333
- bodyObj = { model: chatModel, messages, stream: false, ...(tools?.length ? { tools } : {}) };
1334
- }
1335
- else if (provider === 'deepseek') {
1336
- url = 'https://api.deepseek.com/chat/completions';
1337
- chatHeaders.Authorization = `Bearer ${apiKey}`;
1338
- bodyObj = { model: chatModel, messages, stream: false, ...(tools?.length ? { tools } : {}) };
1339
- }
1340
- else {
1341
- url = `${baseUrl || 'https://api.openai.com/v1'}/chat/completions`;
1342
- if (apiKey) {
1343
- chatHeaders.Authorization = `Bearer ${apiKey}`;
1344
- }
1345
- bodyObj = {
1346
- model: chatModel,
1347
- messages,
1348
- stream: false,
1349
- ...(tools?.length ? { tools } : {}),
1350
- /*
1351
- * `reasoning_effort` used to be pinned to `none` for every custom base URL,
1352
- * to save context and time on a local model. Behind a proxy that fronts a
1353
- * subscription, the same setting turns off the reasoning the model is being
1354
- * used for, or is rejected outright. It is a setting now, and the default is
1355
- * to leave the parameter out and let the endpoint decide.
1356
- */
1357
- ...(this.config.aiReasoningEffort
1358
- ? { reasoning_effort: this.config.aiReasoningEffort }
1359
- : {}),
1360
- };
1361
- }
1362
- const body = JSON.stringify(bodyObj);
1363
- const bodyBuffer = Buffer.from(body, 'utf8');
1364
- chatHeaders['Content-Length'] = bodyBuffer.length;
1365
- const resolved = resolveRequestModule(url);
1366
- if (!resolved) {
1367
- this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
1425
+ const scriptId = (obj.message?.id || '').toString();
1426
+ const ts = parseInt(obj.message?.ts, 10);
1427
+ if (!scriptId || isNaN(ts)) {
1428
+ this.sendTo(obj.from, obj.command, { error: 'No script ID or timestamp' }, obj.callback);
1368
1429
  return;
1369
1430
  }
1370
- const { module: requestModule, isHttps } = resolved;
1371
1431
  try {
1372
- const req = requestModule.request(url, {
1373
- method: 'POST',
1374
- headers: chatHeaders,
1375
- timeout: requestTimeout,
1376
- ...(isHttps && this.config.allowSelfSignedCerts ? { rejectUnauthorized: false } : {}),
1377
- }, res => {
1378
- let data = '';
1379
- res.on('data', (chunk) => {
1380
- data += chunk.toString();
1381
- });
1382
- res.on('end', () => {
1383
- if (res.statusCode === 200) {
1384
- try {
1385
- const parsed = JSON.parse(data);
1386
- let content;
1387
- let tool_calls;
1388
- if (provider === 'anthropic') {
1389
- const translated = (0, anthropicAdapter_1.translateAnthropicResponseToOpenAI)(parsed);
1390
- content = translated.content;
1391
- tool_calls = translated.tool_calls;
1392
- }
1393
- else {
1394
- const message = parsed.choices?.[0]?.message;
1395
- content = message?.content || '';
1396
- tool_calls = message?.tool_calls;
1397
- }
1398
- if (!content && !tool_calls?.length) {
1399
- this.sendTo(obj.from, obj.command, { error: 'Empty response from API' }, obj.callback);
1400
- }
1401
- else {
1402
- this.sendTo(obj.from, obj.command, {
1403
- success: true,
1404
- content,
1405
- ...(tool_calls ? { tool_calls } : {}),
1406
- }, obj.callback);
1407
- }
1408
- }
1409
- catch {
1410
- this.sendTo(obj.from, obj.command, { error: 'Invalid JSON response from API' }, obj.callback);
1411
- }
1412
- }
1413
- else {
1414
- let detail = '';
1415
- try {
1416
- const errParsed = JSON.parse(data);
1417
- detail = errParsed.error?.message || data.substring(0, 200);
1418
- }
1419
- catch {
1420
- detail = data.substring(0, 200);
1421
- }
1422
- this.sendTo(obj.from, obj.command, {
1423
- error: `${detail || httpStatusText(res.statusCode || 0)} (${res.statusCode})`,
1424
- }, obj.callback);
1425
- }
1426
- });
1427
- });
1428
- req.on('error', (err) => {
1429
- this.sendTo(obj.from, obj.command, { error: `Connection failed: ${err.message}` }, obj.callback);
1430
- });
1431
- req.on('timeout', () => {
1432
- req.destroy();
1433
- this.sendTo(obj.from, obj.command, { error: `Connection timeout (${Math.round(requestTimeout / 1000)}s)` }, obj.callback);
1434
- });
1435
- req.write(bodyBuffer);
1436
- req.end();
1432
+ const raw = await this.readFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(scriptId, ts));
1433
+ const source = (0, scriptHistory_1.fileToString)(raw);
1434
+ this.sendTo(obj.from, obj.command, { source }, obj.callback);
1437
1435
  }
1438
- catch (error) {
1439
- this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
1436
+ catch (e) {
1437
+ this.sendTo(obj.from, obj.command, { error: `Cannot read the version: ${e instanceof Error ? e.message : String(e)}` }, obj.callback);
1440
1438
  }
1441
1439
  })();
1442
1440
  break;
1443
1441
  }
1444
- case 'testApiConnection': {
1445
- // Test connection to an OpenAI-compatible API endpoint.
1446
- // The settings-dialog Test button sends the current form value as `apiKey`
1447
- // (so users can test before saving); otherwise we fall back to the stored key.
1442
+ case 'getHistoryUsage': {
1443
+ // Read by the overview in the adapter settings (`textSendTo` with container html)
1448
1444
  void (async () => {
1449
1445
  if (!obj.callback) {
1450
1446
  return;
1451
1447
  }
1452
- const provider = (obj.message?.provider || 'openai').trim();
1453
- const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
1454
- messageApiKey: obj.message?.apiKey,
1455
- messageBaseUrl: obj.message?.baseUrl,
1456
- messageCredentialId: obj.message?.credentialId,
1457
- credentialType: obj.message?.credentialType,
1458
- });
1459
- // Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
1460
- if (!apiKey &&
1461
- (provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
1462
- this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
1463
- return;
1464
- }
1465
- let url;
1466
- const testHeaders = {
1467
- 'Content-Type': 'application/json',
1468
- };
1469
- if (provider === 'anthropic') {
1470
- url = 'https://api.anthropic.com/v1/models';
1471
- testHeaders['x-api-key'] = apiKey;
1472
- testHeaders['anthropic-version'] = '2023-06-01';
1473
- }
1474
- else if (provider === 'gemini') {
1475
- url = 'https://generativelanguage.googleapis.com/v1beta/openai/models';
1476
- if (apiKey) {
1477
- testHeaders.Authorization = `Bearer ${apiKey}`;
1478
- }
1448
+ try {
1449
+ this.sendTo(obj.from, obj.command, await this.collectHistoryUsage(), obj.callback);
1479
1450
  }
1480
- else if (provider === 'deepseek') {
1481
- url = 'https://api.deepseek.com/models';
1482
- testHeaders.Authorization = `Bearer ${apiKey}`;
1451
+ catch (e) {
1452
+ this.sendTo(obj.from, obj.command, { error: `Cannot read the history: ${e instanceof Error ? e.message : String(e)}` }, obj.callback);
1483
1453
  }
1484
- else {
1485
- url = `${baseUrl || 'https://api.openai.com/v1'}/models`;
1486
- if (apiKey) {
1487
- testHeaders.Authorization = `Bearer ${apiKey}`;
1488
- }
1454
+ })();
1455
+ break;
1456
+ }
1457
+ case 'deleteHistory': {
1458
+ // One version, the history of one script, or every history without a script
1459
+ void (async () => {
1460
+ if (!obj.callback) {
1461
+ return;
1489
1462
  }
1490
- const resolved = resolveRequestModule(url);
1491
- if (!resolved) {
1492
- this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
1463
+ const scriptId = (obj.message?.id || '').toString();
1464
+ const orphans = !!obj.message?.orphans;
1465
+ // Wiping every history is a deliberate request, never the default of a
1466
+ // message that forgot to say what it meant
1467
+ const all = !!obj.message?.all;
1468
+ if (!scriptId && !orphans && !all) {
1469
+ this.sendTo(obj.from, obj.command, { error: 'No script ID provided' }, obj.callback);
1493
1470
  return;
1494
1471
  }
1495
- const { module: requestModule, isHttps } = resolved;
1496
1472
  try {
1497
- const req = requestModule.request(url, {
1498
- method: 'GET',
1499
- headers: testHeaders,
1500
- timeout: 10000,
1501
- ...(isHttps && this.config.allowSelfSignedCerts ? { rejectUnauthorized: false } : {}),
1502
- }, res => {
1503
- let data = '';
1504
- res.on('data', (chunk) => {
1505
- data += chunk.toString();
1506
- });
1507
- res.on('end', () => {
1508
- if (res.statusCode === 200) {
1509
- try {
1510
- const parsed = JSON.parse(data);
1511
- const models = (parsed.data || [])
1512
- .map((m) => m.id.startsWith('models/') ? m.id.substring(7) : m.id)
1513
- .sort();
1514
- this.sendTo(obj.from, obj.command, { success: true, models, count: models.length }, obj.callback);
1515
- }
1516
- catch {
1517
- this.sendTo(obj.from, obj.command, { error: 'Invalid JSON response from API' }, obj.callback);
1518
- }
1519
- }
1520
- else if (res.statusCode === 401) {
1521
- this.sendTo(obj.from, obj.command, { error: 'Invalid API key (401)' }, obj.callback);
1522
- }
1523
- else if (res.statusCode === 403) {
1524
- this.sendTo(obj.from, obj.command, { error: 'Access denied (403)' }, obj.callback);
1525
- }
1526
- else {
1527
- // Include response body for debugging
1528
- let detail = '';
1529
- try {
1530
- const errParsed = JSON.parse(data);
1531
- detail = errParsed.error?.message || data.substring(0, 200);
1532
- }
1533
- catch {
1534
- detail = data.substring(0, 200);
1535
- }
1536
- this.sendTo(obj.from, obj.command, {
1537
- error: `${detail || httpStatusText(res.statusCode || 0)} (${res.statusCode})`,
1538
- }, obj.callback);
1539
- }
1540
- });
1541
- });
1542
- req.on('error', (err) => {
1543
- this.sendTo(obj.from, obj.command, { error: `Connection failed: ${err.message}` }, obj.callback);
1473
+ const ts = parseInt(obj.message?.ts, 10);
1474
+ const result = await this.deleteHistory({
1475
+ ...(scriptId ? { id: scriptId } : {}),
1476
+ ...(isNaN(ts) ? {} : { ts }),
1477
+ ...(orphans ? { orphans: true } : {}),
1478
+ ...(all ? { all: true } : {}),
1544
1479
  });
1545
- req.on('timeout', () => {
1546
- req.destroy();
1547
- this.sendTo(obj.from, obj.command, { error: 'Connection timeout (10s)' }, obj.callback);
1548
- });
1549
- req.end();
1480
+ this.sendTo(obj.from, obj.command, result, obj.callback);
1550
1481
  }
1551
- catch (error) {
1552
- this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
1482
+ catch (e) {
1483
+ this.sendTo(obj.from, obj.command, { error: `Cannot delete: ${e instanceof Error ? e.message : String(e)}` }, obj.callback);
1553
1484
  }
1554
1485
  })();
1555
1486
  break;
1556
1487
  }
1557
- case 'getAvailableAiProviders': {
1558
- // Reports which AI providers have stored credentials (keys never leave the backend).
1488
+ case 'getIoBrokerDataDir': {
1559
1489
  if (obj.callback) {
1560
- const providers = (0, aiProviderResolver_1.listAvailableProviders)(this.config);
1561
- this.sendTo(obj.from, obj.command, { providers }, obj.callback);
1490
+ this.sendTo(obj.from, obj.command, {
1491
+ dataDir: (0, adapter_core_1.getAbsoluteDefaultDataDir)(),
1492
+ sep: node_path_1.sep,
1493
+ }, obj.callback);
1562
1494
  }
1563
1495
  break;
1564
1496
  }
@@ -1618,7 +1550,15 @@ class JavaScript extends adapter_core_1.Adapter {
1618
1550
  }
1619
1551
  case 'execute': {
1620
1552
  if (obj.callback) {
1621
- void this.executeScript(obj.message)
1553
+ /*
1554
+ * This runs code on this host with the whole ioBroker API behind it. Anybody who
1555
+ * may send a message here could ask for it, and the message never said on whose
1556
+ * behalf - so there was nothing to check it against. Where the controller names
1557
+ * the user (js-controller 7.2.5 and newer), that user needs the right a command on
1558
+ * the host needs; where it does not, nothing changes.
1559
+ */
1560
+ void this.mayExecuteFor(obj)
1561
+ .then(async (error) => error ? { ok: false, error, logs: [], output: '' } : await this.executeScript(obj.message))
1622
1562
  .then(result => this.sendTo(obj.from, obj.command, result, obj.callback))
1623
1563
  .catch(err => this.sendTo(obj.from, obj.command, {
1624
1564
  ok: false,
@@ -1690,6 +1630,9 @@ class JavaScript extends adapter_core_1.Adapter {
1690
1630
  }
1691
1631
  }
1692
1632
  async main() {
1633
+ await this.ensureHistoryStorage();
1634
+ // Makes a changed limit take effect at once instead of script by script
1635
+ await this.trimAllHistories();
1693
1636
  // Patch the font as it sometimes is wrong
1694
1637
  if (!this.context.debugMode) {
1695
1638
  if (await this.patchFont()) {
@@ -1770,9 +1713,9 @@ class JavaScript extends adapter_core_1.Adapter {
1770
1713
  // Store allowSelfSignedCerts on the context, so sandbox HTTP functions can use it
1771
1714
  // without setting the global process.env.NODE_TLS_REJECT_UNAUTHORIZED (which affects all adapters in compact mode)
1772
1715
  this.context.allowSelfSignedCerts = this.config.allowSelfSignedCerts;
1773
- // In `manager` credential mode, subscribe to the configured AI credentials so changes in the
1774
- // central credential store are picked up live (the keys are cached for the AI sendTo handlers).
1775
- await this.subscribeAiCredentials();
1716
+ // In `manager` credential mode the configured AI credentials are subscribed, so a change in the
1717
+ // central credential store counts without a restart
1718
+ await this.ai.start();
1776
1719
  this.warnAboutMisplacedCustomAiKey();
1777
1720
  // Read the central credential store, so the scripts can use `SECRETS.<name>.<field>`.
1778
1721
  // Later changes are picked up in `onObjectChange`.
@@ -3553,10 +3496,12 @@ class JavaScript extends adapter_core_1.Adapter {
3553
3496
  async debugStop() {
3554
3497
  if (this.debugState.child) {
3555
3498
  this.debugSendToInspector({ cmd: 'end' });
3499
+ // The inspector stops the debugged process first and ends itself only then, because that process writes
3500
+ // its log through its pipes (#2382). Kill it only if it does not manage that in time.
3556
3501
  this.debugState.endTimeout = setTimeout(() => {
3557
3502
  this.debugState.endTimeout = null;
3558
- this.debugState.child?.kill('SIGTERM');
3559
- }, 500);
3503
+ this.debugState.child?.kill('SIGKILL');
3504
+ }, 5_000);
3560
3505
  this.debugState.promiseOnEnd ||= Promise.resolve(0);
3561
3506
  }
3562
3507
  else {