iobroker.javascript 10.3.0 → 10.4.0

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 (161) hide show
  1. package/README.md +20 -9
  2. package/admin/assets/AiChatPanel-BjRIKaXG.js +27 -0
  3. package/admin/assets/{AiChatService-QKcpW4jQ.js → AiChatService-DP1EmVk2.js} +3 -3
  4. package/admin/assets/AiDiffView-DeynDw7L.js +1 -0
  5. package/admin/assets/{AiInlineProvider-DTSPKoXG.js → AiInlineProvider-jkEIJJJ-.js} +1 -1
  6. package/admin/assets/{BlocklyEditor-BZyoN1JE.js → BlocklyEditor-DhwThchX.js} +7 -7
  7. package/admin/assets/Compile-e8N5zI-J.js +165 -0
  8. package/admin/assets/Debugger-BDQUKM1K.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-BjLPNzpK.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--YX6ETsU.js +1 -0
  18. package/admin/assets/{ScriptEditor-CviB6Hif.js → ScriptEditor-BadPC8Mf.js} +2 -2
  19. package/admin/assets/ScriptEditorVanillaMonaco-BnAmy59d.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-Co6AqZwx.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-Ctn5A1AU.js} +1 -1
  28. package/admin/assets/{blocks_convert-0VBjfhqQ.js → blocks_convert-BI0lf0sf.js} +1 -1
  29. package/admin/assets/{blocks_logic-DAo99xDr.js → blocks_logic-o49klUX_.js} +1 -1
  30. package/admin/assets/{blocks_number-C--e19NU.js → blocks_number-DZtyWFyR.js} +1 -1
  31. package/admin/assets/{blocks_object-D0TPhdzg.js → blocks_object-BuzGCxVr.js} +1 -1
  32. package/admin/assets/blocks_procedures-jn9sIYmR.js +3 -0
  33. package/admin/assets/{blocks_sendto-CRIrtOUc.js → blocks_sendto-Coui8w1H.js} +2 -2
  34. package/admin/assets/{blocks_switch-b_GYfF0n.js → blocks_switch-CeRZizTM.js} +1 -1
  35. package/admin/assets/{blocks_system-BdkUdSsc.js → blocks_system-CaBPIC40.js} +1 -1
  36. package/admin/assets/{blocks_text-DSFhEVOf.js → blocks_text-DeDtPYWf.js} +1 -1
  37. package/admin/assets/{blocks_time-CqALAiJp.js → blocks_time-CTXQH3Lm.js} +1 -1
  38. package/admin/assets/{blocks_timeout-yrOD2pZp.js → blocks_timeout-D8IOKOb-.js} +1 -1
  39. package/admin/assets/{blocks_trigger-DeAYXpSS.js → blocks_trigger-zuuXJbhb.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-BwllSRzJ.js} +1 -1
  47. package/admin/assets/hostInit-CrHjJGJJ.js +1 -0
  48. package/admin/assets/index-Bmmscq2G.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-BcFtthDb.js} +23 -23
  52. package/admin/assets/{mf-entry-bootstrap-0-0deef484.js → mf-entry-bootstrap-0-6db6c4fc.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/aiProviderResolver.js +145 -25
  108. package/build/lib/aiProviderResolver.js.map +1 -1
  109. package/build/lib/anthropicAdapter.js +37 -0
  110. package/build/lib/anthropicAdapter.js.map +1 -1
  111. package/build/lib/inspect.js +66 -10
  112. package/build/lib/inspect.js.map +1 -1
  113. package/build/lib/mirror.js +24 -23
  114. package/build/lib/mirror.js.map +1 -1
  115. package/build/lib/scriptHistory.js +245 -0
  116. package/build/lib/scriptHistory.js.map +1 -0
  117. package/build/main.js +575 -31
  118. package/build/main.js.map +1 -1
  119. package/build/types.d.ts +11 -0
  120. package/io-package.json +17 -15
  121. package/package.json +7 -7
  122. package/admin/assets/AiChatPanel-CpY4Bun-.js +0 -27
  123. package/admin/assets/AiDiffView-DscPfZ38.js +0 -1
  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/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
  */
@@ -81,6 +81,7 @@ const sandbox_1 = require("./lib/sandbox");
81
81
  const nodeModulesManagement_1 = require("./lib/nodeModulesManagement");
82
82
  const secrets_1 = require("./lib/secrets");
83
83
  const aiProviderResolver_1 = require("./lib/aiProviderResolver");
84
+ const scriptHistory_1 = require("./lib/scriptHistory");
84
85
  const anthropicAdapter_1 = require("./lib/anthropicAdapter");
85
86
  const eventObj_1 = require("./lib/eventObj");
86
87
  const scheduler_1 = require("./lib/scheduler");
@@ -107,6 +108,11 @@ const forbiddenMirrorLocations = [
107
108
  ];
108
109
  const packageJson = JSON.parse((0, node_fs_1.readFileSync)(`${__dirname}/../package.json`).toString());
109
110
  const SCRIPT_CODE_MARKER = 'script.js.';
111
+ /**
112
+ * Instance-message type the script editor subscribes to, and under which finished AI answers are
113
+ * pushed back to it. Shared verbatim with `src-editor/src/AiChat/AiChatService.ts`.
114
+ */
115
+ const AI_PUSH_MESSAGE_TYPE = 'aiChatAnswer';
110
116
  let webstormDebug;
111
117
  const isCI = !!process.env.CI;
112
118
  // ambient declarations for typescript
@@ -346,6 +352,13 @@ class JavaScript extends adapter_core_1.Adapter {
346
352
  * subscriptions set up in `subscribeAiCredentials`.
347
353
  */
348
354
  aiCredentialCache = new Map();
355
+ /**
356
+ * Editor sessions waiting for pushed AI answers: the session token the editor invented when it
357
+ * subscribed, mapped to the client id the messaging controller gave us. A `chatCompletion`
358
+ * message carries the token, which is how a request is matched back to the browser tab that
359
+ * sent it - the message itself has no socket id.
360
+ */
361
+ aiUiClients = new Map();
349
362
  /** Unsubscribe callbacks for the AI credential subscriptions (manager mode). */
350
363
  aiCredentialUnsubscribers = [];
351
364
  /**
@@ -391,6 +404,16 @@ class JavaScript extends adapter_core_1.Adapter {
391
404
  ...options,
392
405
  name: 'javascript', // adapter name
393
406
  useFormatDate: true,
407
+ /*
408
+ * The script editor subscribes here so that a finished AI answer can be *pushed* to it.
409
+ * An AI request regularly runs longer than 30 s, and `@iobroker/ws` answers every socket
410
+ * callback with the string "timeout" after exactly that (socket.io.js: `Date.now() + 30_000`,
411
+ * not configurable). The answer then arrived in a dead callback and the chat stayed empty
412
+ * with nothing in any log. So the editor gets an immediate acknowledgement and the result
413
+ * later as an instance message.
414
+ */
415
+ uiClientSubscribe: (info) => this.onUiClientSubscribe(info),
416
+ uiClientUnsubscribe: (info) => this.onUiClientUnsubscribe(info),
394
417
  /**
395
418
  * If the JS-Controller catches an unhandled error, this will be called,
396
419
  * so we have a chance to handle it ourselves.
@@ -472,6 +495,13 @@ class JavaScript extends adapter_core_1.Adapter {
472
495
  }
473
496
  }
474
497
  }
498
+ if (debugMode) {
499
+ // This process was started by the inspector (see `debugStart`) with `--debug`, so the whole log goes
500
+ // through the pipes of that process. If it is gone, writing to them fails with EPIPE, and an unhandled
501
+ // EPIPE would end the debugged process with an uncaught exception (#2382)
502
+ process.stdout.on('error', () => { });
503
+ process.stderr.on('error', () => { });
504
+ }
475
505
  this.context = {
476
506
  mods: this.mods,
477
507
  objects: this.objects,
@@ -523,6 +553,220 @@ class JavaScript extends adapter_core_1.Adapter {
523
553
  };
524
554
  this.tsServer = new lazyTsServer_1.LazyTsServer(typescriptSettings_1.tsCompilerOptions, this.tsLog, TS_MEMORY_RELEASE_DELAY_MS);
525
555
  }
556
+ /**
557
+ * Make sure the instance has a file area, which is where the script history lives.
558
+ *
559
+ * The adapter's own namespace needs a `meta` object before anything can be written under it.
560
+ * Deliberately not `javascript.admin`: that is the UI's file area and every
561
+ * `iobroker upload javascript` wipes it.
562
+ */
563
+ async ensureHistoryStorage() {
564
+ try {
565
+ await this.setForeignObjectNotExistsAsync(this.namespace, {
566
+ type: 'meta',
567
+ common: {
568
+ name: 'javascript files',
569
+ type: 'meta.user',
570
+ },
571
+ native: {},
572
+ });
573
+ }
574
+ catch (e) {
575
+ this.log.warn(`Cannot create the file storage of the instance: ${e instanceof Error ? e.message : String(e)}`);
576
+ }
577
+ }
578
+ /**
579
+ * Keep the saved source of a script, so it can be looked at and brought back later.
580
+ *
581
+ * Hooked into the object change and not into the editor on purpose: this way a change from the
582
+ * mirror directory, from the object browser, from another script or from the AI is kept just
583
+ * the same as one typed into the editor.
584
+ *
585
+ * @param id id of the script
586
+ * @param obj the script as it is now
587
+ * @param formerObj the script as it was before this change, if it existed
588
+ */
589
+ async storeScriptVersion(id, obj, formerObj) {
590
+ const keep = (0, scriptHistory_1.resolveHistoryVersions)(this.config.historyVersions);
591
+ if (!keep) {
592
+ return;
593
+ }
594
+ // Only the instance that runs the script writes its history, otherwise two instances
595
+ // store every version twice
596
+ if (obj.common?.engine !== `system.adapter.${this.namespace}`) {
597
+ return;
598
+ }
599
+ const source = obj.common.source;
600
+ if (!(0, scriptHistory_1.isWorthStoring)(formerObj?.common?.source, source)) {
601
+ return;
602
+ }
603
+ try {
604
+ const ts = typeof obj.ts === 'number' && obj.ts ? obj.ts : Date.now();
605
+ const entry = {
606
+ ts,
607
+ ...(obj.from ? { from: obj.from } : {}),
608
+ ...(obj.user ? { user: obj.user } : {}),
609
+ // The source of a protected script is encrypted in the object and is kept that
610
+ // way - the history must not be the place where it lies around in plain text
611
+ ...(0, scriptHistory_1.describeSource)(source, !!obj.native?.protected),
612
+ };
613
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, ts), source);
614
+ const raw = await this.readFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id)).catch(() => null);
615
+ const current = (0, scriptHistory_1.parseIndex)((0, scriptHistory_1.fileToString)(raw));
616
+ const { index, obsolete } = (0, scriptHistory_1.addVersion)(current, entry, keep);
617
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id), JSON.stringify(index));
618
+ for (const old of obsolete) {
619
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, old)).catch(() => {
620
+ /* a version that is already gone is one we do not have to delete */
621
+ });
622
+ }
623
+ this.log.debug(`Script history: stored version ${ts} of ${id} (${index.length} kept)`);
624
+ }
625
+ catch (e) {
626
+ // A history is a convenience; it must never get in the way of saving a script
627
+ this.log.warn(`Cannot store the version of ${id}: ${e instanceof Error ? e.message : String(e)}`);
628
+ }
629
+ }
630
+ /**
631
+ * The stored versions of a script, newest first, without their sources.
632
+ *
633
+ * @param id id of the script
634
+ */
635
+ async readScriptVersions(id) {
636
+ try {
637
+ const raw = await this.readFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id));
638
+ return (0, scriptHistory_1.parseIndex)((0, scriptHistory_1.fileToString)(raw));
639
+ }
640
+ catch {
641
+ // No history yet - not an error, just nothing to show
642
+ return [];
643
+ }
644
+ }
645
+ /** What every stored history occupies, biggest first - read from the indexes, never from the sources */
646
+ async collectHistoryUsage() {
647
+ const rows = [];
648
+ let folders = [];
649
+ try {
650
+ folders = await this.readDirAsync(this.namespace, scriptHistory_1.HISTORY_ROOT);
651
+ }
652
+ catch {
653
+ // Nothing stored yet
654
+ }
655
+ for (const folder of folders) {
656
+ const id = (0, scriptHistory_1.decodeFolderName)(folder.file);
657
+ const index = await this.readScriptVersions(id);
658
+ if (!index.length) {
659
+ continue;
660
+ }
661
+ const { versions, bytes } = (0, scriptHistory_1.sumIndex)(index);
662
+ rows.push({ id, exists: !!this.objects[id], versions, bytes });
663
+ }
664
+ rows.sort((a, b) => b.bytes - a.bytes);
665
+ return {
666
+ rows,
667
+ total: {
668
+ scripts: rows.length,
669
+ versions: rows.reduce((sum, r) => sum + r.versions, 0),
670
+ bytes: rows.reduce((sum, r) => sum + r.bytes, 0),
671
+ },
672
+ };
673
+ }
674
+ /**
675
+ * Remove stored versions.
676
+ *
677
+ * @param options what to remove
678
+ * @param options.id the script whose versions are meant
679
+ * @param options.ts one single version of that script; without it the whole history goes
680
+ * @param options.orphans every history whose script no longer exists
681
+ * @param options.all every stored version there is - the scripts themselves stay untouched
682
+ */
683
+ async deleteHistory(options) {
684
+ let deleted = 0;
685
+ let bytes = 0;
686
+ /**
687
+ * Delete versions of one script and write the remaining index back.
688
+ *
689
+ * @param id the script
690
+ * @param keep the versions that stay; an empty list removes the index as well
691
+ * @param drop the versions to remove
692
+ */
693
+ const apply = async (id, keep, drop) => {
694
+ for (const version of drop) {
695
+ bytes += version.size || 0;
696
+ deleted++;
697
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, version.ts)).catch(() => {
698
+ /* a version that is already gone is one we do not have to delete */
699
+ });
700
+ }
701
+ if (keep.length) {
702
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id), JSON.stringify(keep));
703
+ }
704
+ else {
705
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id)).catch(() => {
706
+ /* already gone */
707
+ });
708
+ }
709
+ };
710
+ if (options.orphans || options.all) {
711
+ const { rows } = await this.collectHistoryUsage();
712
+ // `all` takes everything, `orphans` only what has no script behind it anymore
713
+ for (const row of options.all ? rows : rows.filter(r => !r.exists)) {
714
+ await apply(row.id, [], await this.readScriptVersions(row.id));
715
+ }
716
+ }
717
+ else if (options.id) {
718
+ const index = await this.readScriptVersions(options.id);
719
+ if (options.ts) {
720
+ await apply(options.id, index.filter(v => v.ts !== options.ts), index.filter(v => v.ts === options.ts));
721
+ }
722
+ else {
723
+ await apply(options.id, [], index);
724
+ }
725
+ }
726
+ this.log.debug(`Script history: removed ${deleted} versions (${(0, scriptHistory_1.formatBytes)(bytes)})`);
727
+ return { deleted, bytes };
728
+ }
729
+ /**
730
+ * Cut every history down to the configured number of versions.
731
+ *
732
+ * Without this a smaller limit would only take effect script by script, on the next save of
733
+ * each - and setting it to zero would never free anything at all.
734
+ */
735
+ async trimAllHistories() {
736
+ const keep = (0, scriptHistory_1.resolveHistoryVersions)(this.config.historyVersions);
737
+ let folders = [];
738
+ try {
739
+ folders = await this.readDirAsync(this.namespace, scriptHistory_1.HISTORY_ROOT);
740
+ }
741
+ catch {
742
+ return;
743
+ }
744
+ for (const folder of folders) {
745
+ const id = (0, scriptHistory_1.decodeFolderName)(folder.file);
746
+ const index = await this.readScriptVersions(id);
747
+ if (!index.length) {
748
+ continue;
749
+ }
750
+ const { index: kept, obsolete } = (0, scriptHistory_1.trimIndex)(index, keep);
751
+ if (!obsolete.length) {
752
+ continue;
753
+ }
754
+ for (const ts of obsolete) {
755
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(id, ts)).catch(() => {
756
+ /* already gone */
757
+ });
758
+ }
759
+ if (kept.length) {
760
+ await this.writeFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id), JSON.stringify(kept));
761
+ }
762
+ else {
763
+ await this.delFileAsync(this.namespace, (0, scriptHistory_1.indexPath)(id)).catch(() => {
764
+ /* already gone */
765
+ });
766
+ }
767
+ this.log.debug(`Script history: trimmed ${id} to ${kept.length} versions`);
768
+ }
769
+ }
526
770
  async onObjectChange(id, obj) {
527
771
  // Check if we should ignore this change (once!) because we just updated the compiled sources
528
772
  if (this.ignoreObjectChange.has(id)) {
@@ -612,6 +856,10 @@ class JavaScript extends adapter_core_1.Adapter {
612
856
  }
613
857
  }
614
858
  }
859
+ // Keep the saved source before the branches below decide what else to do with it
860
+ if (obj?.type === 'script') {
861
+ await this.storeScriptVersion(id, obj, formerObj);
862
+ }
615
863
  // handle Script object updates
616
864
  if (!obj && formerObj?.type === 'script') {
617
865
  // Object Deleted just now
@@ -821,7 +1069,9 @@ class JavaScript extends adapter_core_1.Adapter {
821
1069
  }
822
1070
  async onUnload(callback) {
823
1071
  try {
824
- await this.debugStop();
1072
+ // Do not wait for it: the inspector needs a moment to stop the debugged process, and it does that by
1073
+ // itself as soon as the connection to this process is gone. The shutdown must not be delayed by it.
1074
+ void this.debugStop();
825
1075
  this.stopTimeSchedules();
826
1076
  if (this.setStateCountCheckInterval) {
827
1077
  clearInterval(this.setStateCountCheckInterval);
@@ -884,6 +1134,79 @@ class JavaScript extends adapter_core_1.Adapter {
884
1134
  }
885
1135
  await this.main();
886
1136
  }
1137
+ /**
1138
+ * A script editor registers itself for pushed AI answers.
1139
+ *
1140
+ * The editor sends a token it made up; we remember which messaging-controller client it belongs
1141
+ * to. Only our own message type is accepted - anything else is some other UI asking for
1142
+ * something we do not serve.
1143
+ *
1144
+ * @param info client id and the subscribe message, as the messaging controller hands it over
1145
+ * @param info.clientId the id to address this client with later
1146
+ * @param info.message the subscribe message, carrying the type and the editor's session token
1147
+ */
1148
+ onUiClientSubscribe(info) {
1149
+ const message = info.message?.message;
1150
+ if (message?.type !== AI_PUSH_MESSAGE_TYPE) {
1151
+ return { accepted: false, error: `Unknown subscription type "${message?.type || ''}"` };
1152
+ }
1153
+ const token = (message.data?.sessionToken || '').trim();
1154
+ if (!token) {
1155
+ return { accepted: false, error: 'No session token provided' };
1156
+ }
1157
+ this.aiUiClients.set(token, info.clientId);
1158
+ this.log.debug(`AI editor session subscribed for pushed answers (${this.aiUiClients.size} open)`);
1159
+ return { accepted: true };
1160
+ }
1161
+ /**
1162
+ * A script editor went away - drop every token that pointed at it.
1163
+ *
1164
+ * @param info the client the messaging controller is retiring
1165
+ * @param info.clientId the id that is going away
1166
+ */
1167
+ onUiClientUnsubscribe(info) {
1168
+ for (const [token, clientId] of this.aiUiClients) {
1169
+ if (clientId === info.clientId) {
1170
+ this.aiUiClients.delete(token);
1171
+ }
1172
+ }
1173
+ this.log.debug(`AI editor session unsubscribed (${this.aiUiClients.size} open)`);
1174
+ }
1175
+ /**
1176
+ * Build the function that delivers the answer of one `chatCompletion` request.
1177
+ *
1178
+ * If the editor registered for pushed answers and named itself in the message, the answer goes
1179
+ * out as an instance message and the socket callback is acknowledged right away. Everything
1180
+ * else - an older editor, the inline completion, a script calling us - keeps the plain
1181
+ * request/response behaviour.
1182
+ *
1183
+ * @param obj the incoming sendTo message
1184
+ */
1185
+ buildAiResponder(obj) {
1186
+ const token = (obj.message?.uiSession || '').toString().trim();
1187
+ const requestId = (obj.message?.requestId || '').toString().trim();
1188
+ const clientId = token ? this.aiUiClients.get(token) : undefined;
1189
+ if (!clientId || !requestId) {
1190
+ return payload => this.sendTo(obj.from, obj.command, payload, obj.callback);
1191
+ }
1192
+ // Release the socket callback immediately - it has 30 s to live, the request has more.
1193
+ this.sendTo(obj.from, obj.command, { accepted: true, requestId }, obj.callback);
1194
+ let sent = false;
1195
+ return payload => {
1196
+ if (sent) {
1197
+ return;
1198
+ }
1199
+ sent = true;
1200
+ this.sendToUI({
1201
+ clientId,
1202
+ data: { type: AI_PUSH_MESSAGE_TYPE, requestId, ...payload },
1203
+ }).catch(e => {
1204
+ // The tab was closed, or the admin dropped the subscription in the meantime.
1205
+ this.aiUiClients.delete(token);
1206
+ this.log.warn(`Cannot deliver the AI answer to the editor: ${e instanceof Error ? e.message : String(e)}`);
1207
+ });
1208
+ };
1209
+ }
887
1210
  /** Read and decrypt a single AI credential's key from the central store; returns '' (and logs) on error. */
888
1211
  async readAiCredentialKey(id) {
889
1212
  if (!adapter_core_1.Credentials?.getCredentials) {
@@ -892,6 +1215,15 @@ class JavaScript extends adapter_core_1.Adapter {
892
1215
  }
893
1216
  try {
894
1217
  const cred = await adapter_core_1.Credentials.getCredentials(this, id);
1218
+ /*
1219
+ * Only an entry that was stored as an AI credential. The store holds the secrets of the
1220
+ * whole system - a database password, the login of a camera - and nothing but this check
1221
+ * keeps a request for "the key of this provider" from reaching one of them.
1222
+ */
1223
+ if (cred?.type !== 'ai') {
1224
+ this.log.warn(`Cannot read AI credential "${id}": it is not an AI credential`);
1225
+ return '';
1226
+ }
895
1227
  return (cred?.values?.key || '').trim();
896
1228
  }
897
1229
  catch (e) {
@@ -911,12 +1243,12 @@ class JavaScript extends adapter_core_1.Adapter {
911
1243
  * The settings-dialog Test button may pass form values that are not saved yet
912
1244
  * (`messageApiKey` / `messageCredentialId` / `credentialType`); those win over the stored config.
913
1245
  */
914
- async resolveAiCredentials(provider, opts = {}) {
915
- const mode = opts.credentialType || this.config.credentialType || 'manual';
1246
+ async resolveAiCredentials(provider) {
1247
+ const mode = this.config.credentialType || 'manual';
916
1248
  if (mode === 'manager') {
917
1249
  // 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();
1250
+ const { baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider);
1251
+ const id = (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider).trim();
920
1252
  if (!id) {
921
1253
  return { apiKey: '', baseUrl };
922
1254
  }
@@ -925,11 +1257,74 @@ class JavaScript extends adapter_core_1.Adapter {
925
1257
  const apiKey = cached !== undefined ? cached : await this.readAiCredentialKey(id);
926
1258
  return { apiKey, baseUrl };
927
1259
  }
1260
+ return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider);
1261
+ }
1262
+ /**
1263
+ * Endpoint and key for the Test button of the settings dialog.
1264
+ *
1265
+ * The dialog tries what stands in the form - a key that was typed but not saved, a credential that
1266
+ * was chosen but not saved, an endpoint that was entered but not saved. What the form may decide is
1267
+ * limited by `resolveTestEndpoint`: a secret of this system is never carried to an address that
1268
+ * came with the message. A chat request takes none of this and reads the configuration instead -
1269
+ * see `resolveAiCredentials`.
1270
+ *
1271
+ * @param provider the provider that is being tested
1272
+ * @param opts what the Test button sent
1273
+ * @param opts.messageBaseUrl the address that stands in the form
1274
+ * @param opts.messageApiKey the key that stands in the form
1275
+ * @param opts.messageCredentialId the entry of the credential store that the form names
1276
+ * @param opts.credentialType where the form says the keys are kept
1277
+ */
1278
+ async resolveAiTestCredentials(provider, opts = {}) {
1279
+ const mode = opts.credentialType || this.config.credentialType || 'manual';
1280
+ if (mode === 'manager') {
1281
+ // A key out of the store is a secret of this system, so the address is the stored one
1282
+ const { baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider);
1283
+ const id = (opts.messageCredentialId || (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider)).trim();
1284
+ if (!id) {
1285
+ return { apiKey: '', baseUrl };
1286
+ }
1287
+ const cached = this.aiCredentialCache.get(id);
1288
+ const apiKey = cached !== undefined ? cached : await this.readAiCredentialKey(id);
1289
+ return { apiKey, baseUrl };
1290
+ }
928
1291
  // Manual mode. The Test button sends the current form key (maybe empty) — let it win.
929
1292
  if (opts.messageApiKey !== undefined) {
930
1293
  return (0, aiProviderResolver_1.resolveTestCredentials)(this.config, provider, opts.messageApiKey, opts.messageBaseUrl);
931
1294
  }
932
- return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
1295
+ return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider);
1296
+ }
1297
+ /**
1298
+ * Whether the user a message was sent on behalf of may have code executed here.
1299
+ *
1300
+ * `obj.user` arrives with js-controller 7.2.5 and newer: a socket server such as `admin` or `web`
1301
+ * puts the authenticated user of the connection there, and a socket client cannot set it itself.
1302
+ * Where it is missing - an older controller, a script, an adapter that names nobody - nothing is
1303
+ * checked and everything stays as it was.
1304
+ *
1305
+ * The right asked for is the one `cmdExec` is checked against: running a script here is running
1306
+ * code on this host, whatever the ACL of the single objects it then touches says.
1307
+ *
1308
+ * @param obj the incoming message
1309
+ */
1310
+ async mayExecuteFor(obj) {
1311
+ // `user` is not in the older `@iobroker/types` this adapter builds against
1312
+ const user = obj.user;
1313
+ if (!user) {
1314
+ return null;
1315
+ }
1316
+ try {
1317
+ const permissions = await this.calculatePermissionsAsync(user, {
1318
+ cmdExec: { type: 'other', operation: 'execute' },
1319
+ });
1320
+ if (permissions?.other?.execute) {
1321
+ return null;
1322
+ }
1323
+ return `User "${user}" has no permission to execute scripts`;
1324
+ }
1325
+ catch (e) {
1326
+ return `Cannot check the permissions of "${user}": ${e instanceof Error ? e.message : String(e)}`;
1327
+ }
933
1328
  }
934
1329
  /**
935
1330
  * Warns about the one setup that the fix for #2369 stops working.
@@ -1268,6 +1663,92 @@ class JavaScript extends adapter_core_1.Adapter {
1268
1663
  this.context.rulesOpened = null;
1269
1664
  break;
1270
1665
  }
1666
+ case 'getScriptVersions': {
1667
+ // The stored versions of one script, without their sources - the list must stay
1668
+ // cheap, a history of 30 Blockly versions is megabytes
1669
+ void (async () => {
1670
+ if (!obj.callback) {
1671
+ return;
1672
+ }
1673
+ const scriptId = (obj.message?.id || '').toString();
1674
+ if (!scriptId) {
1675
+ this.sendTo(obj.from, obj.command, { error: 'No script ID provided' }, obj.callback);
1676
+ return;
1677
+ }
1678
+ const versions = await this.readScriptVersions(scriptId);
1679
+ this.sendTo(obj.from, obj.command, { versions }, obj.callback);
1680
+ })();
1681
+ break;
1682
+ }
1683
+ case 'getScriptVersion': {
1684
+ // The source of one stored version, fetched when it is actually looked at
1685
+ void (async () => {
1686
+ if (!obj.callback) {
1687
+ return;
1688
+ }
1689
+ const scriptId = (obj.message?.id || '').toString();
1690
+ const ts = parseInt(obj.message?.ts, 10);
1691
+ if (!scriptId || isNaN(ts)) {
1692
+ this.sendTo(obj.from, obj.command, { error: 'No script ID or timestamp' }, obj.callback);
1693
+ return;
1694
+ }
1695
+ try {
1696
+ const raw = await this.readFileAsync(this.namespace, (0, scriptHistory_1.versionPath)(scriptId, ts));
1697
+ const source = (0, scriptHistory_1.fileToString)(raw);
1698
+ this.sendTo(obj.from, obj.command, { source }, obj.callback);
1699
+ }
1700
+ catch (e) {
1701
+ this.sendTo(obj.from, obj.command, { error: `Cannot read the version: ${e instanceof Error ? e.message : String(e)}` }, obj.callback);
1702
+ }
1703
+ })();
1704
+ break;
1705
+ }
1706
+ case 'getHistoryUsage': {
1707
+ // Read by the overview in the adapter settings (`textSendTo` with container html)
1708
+ void (async () => {
1709
+ if (!obj.callback) {
1710
+ return;
1711
+ }
1712
+ try {
1713
+ this.sendTo(obj.from, obj.command, await this.collectHistoryUsage(), obj.callback);
1714
+ }
1715
+ catch (e) {
1716
+ this.sendTo(obj.from, obj.command, { error: `Cannot read the history: ${e instanceof Error ? e.message : String(e)}` }, obj.callback);
1717
+ }
1718
+ })();
1719
+ break;
1720
+ }
1721
+ case 'deleteHistory': {
1722
+ // One version, the history of one script, or every history without a script
1723
+ void (async () => {
1724
+ if (!obj.callback) {
1725
+ return;
1726
+ }
1727
+ const scriptId = (obj.message?.id || '').toString();
1728
+ const orphans = !!obj.message?.orphans;
1729
+ // Wiping every history is a deliberate request, never the default of a
1730
+ // message that forgot to say what it meant
1731
+ const all = !!obj.message?.all;
1732
+ if (!scriptId && !orphans && !all) {
1733
+ this.sendTo(obj.from, obj.command, { error: 'No script ID provided' }, obj.callback);
1734
+ return;
1735
+ }
1736
+ try {
1737
+ const ts = parseInt(obj.message?.ts, 10);
1738
+ const result = await this.deleteHistory({
1739
+ ...(scriptId ? { id: scriptId } : {}),
1740
+ ...(isNaN(ts) ? {} : { ts }),
1741
+ ...(orphans ? { orphans: true } : {}),
1742
+ ...(all ? { all: true } : {}),
1743
+ });
1744
+ this.sendTo(obj.from, obj.command, result, obj.callback);
1745
+ }
1746
+ catch (e) {
1747
+ this.sendTo(obj.from, obj.command, { error: `Cannot delete: ${e instanceof Error ? e.message : String(e)}` }, obj.callback);
1748
+ }
1749
+ })();
1750
+ break;
1751
+ }
1271
1752
  case 'getIoBrokerDataDir': {
1272
1753
  if (obj.callback) {
1273
1754
  this.sendTo(obj.from, obj.command, {
@@ -1283,25 +1764,33 @@ class JavaScript extends adapter_core_1.Adapter {
1283
1764
  // credentials manager — they never leave the adapter (frontend only sends `provider`).
1284
1765
  void (async () => {
1285
1766
  if (!obj.callback) {
1767
+ this.log.warn(`chatCompletion from ${obj.from} came without a callback - nobody can receive the answer`);
1286
1768
  return;
1287
1769
  }
1288
1770
  const chatModel = (obj.message?.model || '').trim();
1289
1771
  const messages = obj.message?.messages;
1290
1772
  const tools = obj.message?.tools;
1291
1773
  const provider = (obj.message?.provider || 'openai').trim();
1774
+ // One line per request, so an empty chat panel can be told apart from a request
1775
+ // that never arrived here. Without it the log stays silent either way.
1776
+ this.log.debug(`chatCompletion from ${obj.from}: ${provider}/${chatModel}, ${Array.isArray(messages) ? messages.length : 0} messages, ${Array.isArray(tools) ? tools.length : 0} tools`);
1777
+ // Answers go through this from here on: either straight back through the socket
1778
+ // callback, or - for an editor that registered for it - pushed as an instance
1779
+ // message once the endpoint is done. See `buildAiResponder`.
1780
+ const respond = this.buildAiResponder(obj);
1292
1781
  // How long the caller is willing to wait - see `resolveRequestTimeout`
1293
1782
  const requestTimeout = (0, aiProviderResolver_1.resolveRequestTimeout)(obj.message?.timeout);
1294
- const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
1295
- messageBaseUrl: obj.message?.baseUrl,
1296
- });
1783
+ const maxTokens = (0, aiProviderResolver_1.resolveMaxTokens)(this.config.aiMaxTokens);
1784
+ // Endpoint and key come from the configuration; a request cannot name either
1785
+ const { apiKey, baseUrl } = await this.resolveAiCredentials(provider);
1297
1786
  // Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
1298
1787
  if (!apiKey &&
1299
1788
  (provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
1300
- this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
1789
+ respond({ error: 'No API key provided' });
1301
1790
  return;
1302
1791
  }
1303
1792
  if (!chatModel || !messages) {
1304
- this.sendTo(obj.from, obj.command, { error: 'Model and messages are required' }, obj.callback);
1793
+ respond({ error: 'Model and messages are required' });
1305
1794
  return;
1306
1795
  }
1307
1796
  let url;
@@ -1318,7 +1807,15 @@ class JavaScript extends adapter_core_1.Adapter {
1318
1807
  const anthropicTools = tools?.length ? (0, anthropicAdapter_1.translateToolsToAnthropic)(tools) : [];
1319
1808
  bodyObj = {
1320
1809
  model: chatModel,
1321
- max_tokens: 8192,
1810
+ /*
1811
+ * Anthropic requires `max_tokens`, so unlike the other providers it
1812
+ * cannot be left to the endpoint. 8192 is the default because it is
1813
+ * what every Anthropic model accepts - the older small ones cap out
1814
+ * below that and answer 400 to anything higher. The newer models write
1815
+ * far longer answers than that, and a script that runs into the limit
1816
+ * is simply cut off mid-line, so the value is a setting.
1817
+ */
1818
+ max_tokens: maxTokens,
1322
1819
  stream: false,
1323
1820
  ...(systemText ? { system: systemText } : {}),
1324
1821
  messages: anthropicMessages,
@@ -1364,7 +1861,7 @@ class JavaScript extends adapter_core_1.Adapter {
1364
1861
  chatHeaders['Content-Length'] = bodyBuffer.length;
1365
1862
  const resolved = resolveRequestModule(url);
1366
1863
  if (!resolved) {
1367
- this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
1864
+ respond({ error: `Invalid API URL: ${url}` });
1368
1865
  return;
1369
1866
  }
1370
1867
  const { module: requestModule, isHttps } = resolved;
@@ -1395,19 +1892,39 @@ class JavaScript extends adapter_core_1.Adapter {
1395
1892
  content = message?.content || '';
1396
1893
  tool_calls = message?.tool_calls;
1397
1894
  }
1398
- if (!content && !tool_calls?.length) {
1399
- this.sendTo(obj.from, obj.command, { error: 'Empty response from API' }, obj.callback);
1895
+ // How the endpoint says it finished. Passed on in both
1896
+ // cases, so the editor can tell a truncated answer from
1897
+ // a complete one instead of just showing what arrived.
1898
+ const info = (0, aiProviderResolver_1.extractAiResponseInfo)(parsed);
1899
+ // `.trim()`, because a whitespace-only answer is just as
1900
+ // empty to the user but used to slip past this check and
1901
+ // arrive in the editor as a blank chat bubble with no
1902
+ // reason attached - the stop reason below is the whole point.
1903
+ if (!content.trim() && !tool_calls?.length) {
1904
+ const error = (0, aiProviderResolver_1.describeEmptyAiResponse)(info, data);
1905
+ // What arrived instead of an answer. Without this the
1906
+ // log only repeats what is already known - that there
1907
+ // is nothing - instead of naming the blocks that came
1908
+ const shape = provider === 'anthropic'
1909
+ ? (0, anthropicAdapter_1.describeAnthropicContent)(parsed)
1910
+ : `choices: ${parsed.choices?.length ?? 0}, content: ${typeof parsed
1911
+ .choices?.[0]?.message?.content}`;
1912
+ this.log.warn(`chatCompletion (${provider}/${chatModel}): ${error}. Answer contained: ${shape}`);
1913
+ respond({ error, ...info });
1400
1914
  }
1401
1915
  else {
1402
- this.sendTo(obj.from, obj.command, {
1916
+ this.log.debug(`chatCompletion (${provider}/${chatModel}): answered with ${content.length} characters, ${tool_calls?.length || 0} tool calls, stop reason ${info.finishReason || '-'}`);
1917
+ respond({
1403
1918
  success: true,
1404
1919
  content,
1405
1920
  ...(tool_calls ? { tool_calls } : {}),
1406
- }, obj.callback);
1921
+ ...info,
1922
+ });
1407
1923
  }
1408
1924
  }
1409
- catch {
1410
- this.sendTo(obj.from, obj.command, { error: 'Invalid JSON response from API' }, obj.callback);
1925
+ catch (e) {
1926
+ this.log.warn(`chatCompletion (${provider}/${chatModel}): cannot read the answer of ${url}: ${e instanceof Error ? e.message : String(e)}. Response: ${data.substring(0, 200)}`);
1927
+ respond({ error: 'Invalid JSON response from API' });
1411
1928
  }
1412
1929
  }
1413
1930
  else {
@@ -1419,24 +1936,28 @@ class JavaScript extends adapter_core_1.Adapter {
1419
1936
  catch {
1420
1937
  detail = data.substring(0, 200);
1421
1938
  }
1422
- this.sendTo(obj.from, obj.command, {
1423
- error: `${detail || httpStatusText(res.statusCode || 0)} (${res.statusCode})`,
1424
- }, obj.callback);
1939
+ const error = `${detail || httpStatusText(res.statusCode || 0)} (${res.statusCode})`;
1940
+ this.log.warn(`chatCompletion (${provider}/${chatModel}): ${error}`);
1941
+ respond({ error });
1425
1942
  }
1426
1943
  });
1427
1944
  });
1428
1945
  req.on('error', (err) => {
1429
- this.sendTo(obj.from, obj.command, { error: `Connection failed: ${err.message}` }, obj.callback);
1946
+ this.log.warn(`chatCompletion (${provider}/${chatModel}): cannot reach ${url}: ${err.message}`);
1947
+ respond({ error: `Connection failed: ${err.message}` });
1430
1948
  });
1431
1949
  req.on('timeout', () => {
1432
1950
  req.destroy();
1433
- this.sendTo(obj.from, obj.command, { error: `Connection timeout (${Math.round(requestTimeout / 1000)}s)` }, obj.callback);
1951
+ const error = `Connection timeout (${Math.round(requestTimeout / 1000)}s)`;
1952
+ this.log.warn(`chatCompletion (${provider}/${chatModel}): ${url} - ${error}`);
1953
+ respond({ error });
1434
1954
  });
1435
1955
  req.write(bodyBuffer);
1436
1956
  req.end();
1437
1957
  }
1438
1958
  catch (error) {
1439
- this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
1959
+ this.log.warn(`chatCompletion (${provider}/${chatModel}): ${url} - ${error.toString()}`);
1960
+ respond({ error: `Connection failed: ${error.toString()}` });
1440
1961
  }
1441
1962
  })();
1442
1963
  break;
@@ -1450,7 +1971,7 @@ class JavaScript extends adapter_core_1.Adapter {
1450
1971
  return;
1451
1972
  }
1452
1973
  const provider = (obj.message?.provider || 'openai').trim();
1453
- const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
1974
+ const { apiKey, baseUrl } = await this.resolveAiTestCredentials(provider, {
1454
1975
  messageApiKey: obj.message?.apiKey,
1455
1976
  messageBaseUrl: obj.message?.baseUrl,
1456
1977
  messageCredentialId: obj.message?.credentialId,
@@ -1459,7 +1980,17 @@ class JavaScript extends adapter_core_1.Adapter {
1459
1980
  // Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
1460
1981
  if (!apiKey &&
1461
1982
  (provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
1462
- this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
1983
+ // Say which of the two setups came up empty - "No API key provided" sent the user
1984
+ // looking for a key field that `manager` mode does not even show.
1985
+ const mode = obj.message?.credentialType || this.config.credentialType || 'manual';
1986
+ let error = 'No API key provided';
1987
+ if (mode === 'manager') {
1988
+ const id = (obj.message?.credentialId || (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider)).trim();
1989
+ error = id
1990
+ ? `Credential "${id}" contains no API key`
1991
+ : `No credential selected for "${provider}"`;
1992
+ }
1993
+ this.sendTo(obj.from, obj.command, { error }, obj.callback);
1463
1994
  return;
1464
1995
  }
1465
1996
  let url;
@@ -1618,7 +2149,15 @@ class JavaScript extends adapter_core_1.Adapter {
1618
2149
  }
1619
2150
  case 'execute': {
1620
2151
  if (obj.callback) {
1621
- void this.executeScript(obj.message)
2152
+ /*
2153
+ * This runs code on this host with the whole ioBroker API behind it. Anybody who
2154
+ * may send a message here could ask for it, and the message never said on whose
2155
+ * behalf - so there was nothing to check it against. Where the controller names
2156
+ * the user (js-controller 7.2.5 and newer), that user needs the right a command on
2157
+ * the host needs; where it does not, nothing changes.
2158
+ */
2159
+ void this.mayExecuteFor(obj)
2160
+ .then(async (error) => error ? { ok: false, error, logs: [], output: '' } : await this.executeScript(obj.message))
1622
2161
  .then(result => this.sendTo(obj.from, obj.command, result, obj.callback))
1623
2162
  .catch(err => this.sendTo(obj.from, obj.command, {
1624
2163
  ok: false,
@@ -1690,6 +2229,9 @@ class JavaScript extends adapter_core_1.Adapter {
1690
2229
  }
1691
2230
  }
1692
2231
  async main() {
2232
+ await this.ensureHistoryStorage();
2233
+ // Makes a changed limit take effect at once instead of script by script
2234
+ await this.trimAllHistories();
1693
2235
  // Patch the font as it sometimes is wrong
1694
2236
  if (!this.context.debugMode) {
1695
2237
  if (await this.patchFont()) {
@@ -3553,10 +4095,12 @@ class JavaScript extends adapter_core_1.Adapter {
3553
4095
  async debugStop() {
3554
4096
  if (this.debugState.child) {
3555
4097
  this.debugSendToInspector({ cmd: 'end' });
4098
+ // The inspector stops the debugged process first and ends itself only then, because that process writes
4099
+ // its log through its pipes (#2382). Kill it only if it does not manage that in time.
3556
4100
  this.debugState.endTimeout = setTimeout(() => {
3557
4101
  this.debugState.endTimeout = null;
3558
- this.debugState.child?.kill('SIGTERM');
3559
- }, 500);
4102
+ this.debugState.child?.kill('SIGKILL');
4103
+ }, 5_000);
3560
4104
  this.debugState.promiseOnEnd ||= Promise.resolve(0);
3561
4105
  }
3562
4106
  else {