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.
- package/README.md +23 -22
- package/admin/assets/AiChatPanel-CfjSsVpE.js +27 -0
- package/admin/assets/AiChatService-rFZwYNPk.js +216 -0
- package/admin/assets/AiDiffView-DeynDw7L.js +1 -0
- package/admin/assets/AiInlineProvider-R_AVYwFT.js +5 -0
- package/admin/assets/{BlocklyEditor-BZyoN1JE.js → BlocklyEditor-DPJkoxjP.js} +7 -7
- package/admin/assets/Compile-BgTrxmPA.js +165 -0
- package/admin/assets/Debugger-CThCzBvP.js +15 -0
- package/admin/assets/Diff--plofAtP.js +1 -0
- package/admin/assets/{Documentation-uz4Ut9pT.js → Documentation-D3E0U4AK.js} +4 -4
- package/admin/assets/ExpandMore-Dio4GeHC.js +1 -0
- package/admin/assets/{FbEditor-aOZ-RpPV.js → FbEditor-A7I5U0hA.js} +6 -6
- package/admin/assets/History-Ewi5YEEn.js +1 -0
- package/admin/assets/Import-6Mm2vrM2.js +1 -0
- package/admin/assets/MonacoDiff-BYvG7IqB.js +1 -0
- package/admin/assets/{NotificationsActive-BdzH44gp.js → NotificationsActive-CuNfWVJM.js} +1 -1
- package/admin/assets/RulesEditor-MDkGXERp.js +1 -0
- package/admin/assets/{ScriptEditor-CviB6Hif.js → ScriptEditor-CqphehA8.js} +2 -2
- package/admin/assets/ScriptEditorVanillaMonaco-CxgNa-3O.js +3 -0
- package/admin/assets/Send-Pz1eveU5.js +1 -0
- package/admin/assets/_virtual_mf-localSharedImportMap___mfe_internal__iobroker_javascript__mf_owner__1-Ceh8EDcF.js +1 -0
- 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
- 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
- package/admin/assets/aiPromptBuilder-CI2ChET0.js +1 -0
- package/admin/assets/{blockly-fcfyKYzp.js → blockly-B0t_IRJQ.js} +1 -1
- package/admin/assets/{blocklyTheme-CGhU3cEO.js → blocklyTheme-iZoge1S9.js} +1 -1
- package/admin/assets/{blocks_action-BQ4O7NRE.js → blocks_action-3CepYoiX.js} +1 -1
- package/admin/assets/{blocks_convert-0VBjfhqQ.js → blocks_convert-BEpxVx-_.js} +1 -1
- package/admin/assets/{blocks_logic-DAo99xDr.js → blocks_logic-CyIBT53p.js} +1 -1
- package/admin/assets/{blocks_number-C--e19NU.js → blocks_number-yqm4qwfh.js} +1 -1
- package/admin/assets/{blocks_object-D0TPhdzg.js → blocks_object-CizLkmWp.js} +1 -1
- package/admin/assets/blocks_procedures-pFTThYqz.js +3 -0
- package/admin/assets/{blocks_sendto-CRIrtOUc.js → blocks_sendto-rEq1xPmO.js} +2 -2
- package/admin/assets/{blocks_switch-b_GYfF0n.js → blocks_switch-D3aybjqu.js} +1 -1
- package/admin/assets/{blocks_system-BdkUdSsc.js → blocks_system-BVRsS2OE.js} +1 -1
- package/admin/assets/{blocks_text-DSFhEVOf.js → blocks_text-D93xQ_03.js} +1 -1
- package/admin/assets/{blocks_time-CqALAiJp.js → blocks_time-ChxhjgD7.js} +1 -1
- package/admin/assets/{blocks_timeout-yrOD2pZp.js → blocks_timeout-CUZ80HaP.js} +1 -1
- package/admin/assets/{blocks_trigger-DeAYXpSS.js → blocks_trigger-DivLpAuy.js} +2 -2
- package/admin/assets/{cronHoverProvider-DoO2_tpG.js → cronHoverProvider-Curcjz4m.js} +1 -1
- package/admin/assets/dist-CzitsFi7.js +1 -0
- package/admin/assets/field_cron-RgLY53_S.js +1 -0
- package/admin/assets/field_oid-rBhotXJe.js +1 -0
- package/admin/assets/field_script-CU7g3-oB.js +1 -0
- package/admin/assets/helpers-BZgjFQNf.js +1 -0
- package/admin/assets/{helpers-BTGaTdnT.js → helpers-Bt4VKZBw.js} +1 -1
- package/admin/assets/hostInit-CrHjJGJJ.js +1 -0
- package/admin/assets/index-DzAQnPTD.js +157 -0
- package/admin/assets/{inlineChatWidget-BwQVCPkx.js → inlineChatWidget-ChchJD9a.js} +1 -1
- package/admin/assets/{inlineDiffController-BsDDsDHn.js → inlineDiffController-ly6qX4jk.js} +1 -1
- package/admin/assets/{loadBlocks-RpqM7PwO.js → loadBlocks-DDLPEDa6.js} +23 -23
- package/admin/assets/{mf-entry-bootstrap-0-0deef484.js → mf-entry-bootstrap-0-d055f05d.js} +3 -3
- package/admin/assets/pendingShares-mBGPbRwZ.js +2 -0
- package/admin/assets/{reactour.esm-DX9ZgYlx.js → reactour.esm-Dk3aGxKn.js} +5 -5
- package/admin/assets/{stateHoverProvider-BEadnSXp.js → stateHoverProvider-wAsP-THx.js} +1 -1
- package/admin/assets/utils-DUKzLJUz.js +4 -0
- package/admin/assets/vite-preload-helper-DjNwdemH.js +1 -0
- package/admin/custom/assets/Components-fDcxCyN4.js +1 -0
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomJavascriptSet__mf_owner__1-BraOrMSu.js +1 -0
- 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
- 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
- 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
- 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
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare__date_mf_2_fns__loadShare__.js-Djg9OMcQ.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare__leaflet__loadShare__.js-D9EyCd42.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__loadShare__shared-Hb6V6BnJ.js +2 -0
- 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
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__198282747311932__prebuild__leaflet__prebuild__-CHjWtrkp.js +1 -0
- 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
- package/admin/custom/assets/dist-CzitsFi7.js +1 -0
- package/admin/custom/assets/helpers-BZgjFQNf.js +1 -0
- package/admin/custom/assets/hostInit-akwaQLq8.js +1 -0
- package/admin/custom/assets/index-BkItz-nL.js +1 -0
- package/admin/custom/assets/{lib-BmDlTzq2.js → lib-CADOx2YN.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-ad1cd7d0.js → mf-entry-bootstrap-0-3c4565e0.js} +3 -3
- package/admin/custom/assets/pendingShares-TCMDgBLn.js +2 -0
- package/admin/custom/assets/utils-DUKzLJUz.js +4 -0
- package/admin/custom/assets/vite-preload-helper-DjNwdemH.js +1 -0
- package/admin/custom/customComponents.js +2 -2
- package/admin/custom/i18n/de.json +19 -1
- package/admin/custom/i18n/en.json +19 -1
- package/admin/custom/i18n/es.json +19 -1
- package/admin/custom/i18n/fr.json +19 -1
- package/admin/custom/i18n/it.json +19 -1
- package/admin/custom/i18n/nl.json +19 -1
- package/admin/custom/i18n/pl.json +19 -1
- package/admin/custom/i18n/pt.json +19 -1
- package/admin/custom/i18n/ru.json +19 -1
- package/admin/custom/i18n/uk.json +19 -1
- package/admin/custom/i18n/zh-cn.json +19 -1
- package/admin/i18n/de.json +13 -1
- package/admin/i18n/en.json +13 -1
- package/admin/i18n/es.json +13 -1
- package/admin/i18n/fr.json +13 -1
- package/admin/i18n/it.json +13 -1
- package/admin/i18n/nl.json +13 -1
- package/admin/i18n/pl.json +13 -1
- package/admin/i18n/pt.json +13 -1
- package/admin/i18n/ru.json +13 -1
- package/admin/i18n/uk.json +13 -1
- package/admin/i18n/zh-cn.json +13 -1
- package/admin/jsonConfig.json +274 -86
- package/admin/mf-manifest.json +1 -1
- package/admin/mf-stats.json +1 -1
- package/admin/remoteEntry.js +2 -2
- package/admin/tab.html +2 -2
- package/build/lib/ai.js +53 -0
- package/build/lib/ai.js.map +1 -0
- package/build/lib/inspect.js +66 -10
- package/build/lib/inspect.js.map +1 -1
- package/build/lib/mirror.js +24 -23
- package/build/lib/mirror.js.map +1 -1
- package/build/lib/scriptHistory.js +245 -0
- package/build/lib/scriptHistory.js.map +1 -0
- package/build/main.js +358 -413
- package/build/main.js.map +1 -1
- package/build/types.d.ts +11 -0
- package/io-package.json +17 -15
- package/package.json +8 -7
- package/admin/assets/AiChatPanel-CpY4Bun-.js +0 -27
- package/admin/assets/AiChatService-QKcpW4jQ.js +0 -216
- package/admin/assets/AiDiffView-DscPfZ38.js +0 -1
- package/admin/assets/AiInlineProvider-DTSPKoXG.js +0 -5
- package/admin/assets/Compile-Oss28m_f.js +0 -165
- package/admin/assets/Debugger-teJTIU-2.js +0 -15
- package/admin/assets/ExpandMore-V5Ix1QB1.js +0 -1
- package/admin/assets/Import-BIC5Ee9z.js +0 -1
- package/admin/assets/RulesEditor-CcDVthPa.js +0 -1
- package/admin/assets/ScriptEditorVanillaMonaco-ByuXwybM.js +0 -3
- package/admin/assets/Send-Mdo32jMb.js +0 -1
- package/admin/assets/_virtual_mf-localSharedImportMap___mfe_internal__iobroker_javascript__mf_owner__1-BnL93BgK.js +0 -1
- 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
- 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
- package/admin/assets/aiPromptBuilder-yTB6N2VT.js +0 -1
- package/admin/assets/blocks_procedures-DgTxgXdB.js +0 -3
- package/admin/assets/dist-D4uJqRBO.js +0 -4
- package/admin/assets/field_cron-D3Ts_s9M.js +0 -1
- package/admin/assets/field_oid-BRNXredg.js +0 -1
- package/admin/assets/field_script-3X6ousg7.js +0 -1
- package/admin/assets/hostInit-2h2Cbsaw.js +0 -1
- package/admin/assets/index-CbQlIKB_.js +0 -156
- package/admin/assets/pendingShares-BaAadIDv.js +0 -1
- package/admin/assets/vite-preload-helper-CaC3yEL3.js +0 -1
- package/admin/custom/assets/Components-FkGDIlId.js +0 -1
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomJavascriptSet__mf_owner__1-Cs88NUCd.js +0 -1
- 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
- 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
- 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
- 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
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare__date_mf_2_fns__loadShare__.js-CnhNjamd.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__loadShare__leaflet__loadShare__.js-Dz7BFJqH.js +0 -1
- 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
- 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
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomJavascriptSet__mf_owner__37453319418803__prebuild__leaflet__prebuild__-xPcBaiTQ.js +0 -1
- 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
- package/admin/custom/assets/dist-D4uJqRBO.js +0 -4
- package/admin/custom/assets/hostInit-6oh4UY2k.js +0 -1
- package/admin/custom/assets/index-BrlyEy6J.js +0 -1
- package/admin/custom/assets/pendingShares-kV2kwejX.js +0 -1
- package/admin/custom/assets/vite-preload-helper-CaC3yEL3.js +0 -1
- package/build/lib/aiProviderResolver.js +0 -140
- package/build/lib/aiProviderResolver.js.map +0 -1
- package/build/lib/anthropicAdapter.js +0 -175
- package/build/lib/anthropicAdapter.js.map +0 -1
- /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-
|
|
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
|
|
84
|
-
const
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
/**
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
895
|
-
|
|
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
|
-
|
|
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
|
|
949
|
-
const
|
|
950
|
-
|
|
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
|
-
|
|
961
|
-
|
|
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 '
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1308
|
-
const
|
|
1309
|
-
|
|
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
|
|
1373
|
-
|
|
1374
|
-
|
|
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 (
|
|
1439
|
-
this.sendTo(obj.from, obj.command, { error: `
|
|
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 '
|
|
1445
|
-
//
|
|
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
|
-
|
|
1453
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
...(
|
|
1502
|
-
|
|
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
|
-
|
|
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 (
|
|
1552
|
-
this.sendTo(obj.from, obj.command, { error: `
|
|
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 '
|
|
1558
|
-
// Reports which AI providers have stored credentials (keys never leave the backend).
|
|
1488
|
+
case 'getIoBrokerDataDir': {
|
|
1559
1489
|
if (obj.callback) {
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
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
|
|
1774
|
-
// central credential store
|
|
1775
|
-
await this.
|
|
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('
|
|
3559
|
-
},
|
|
3503
|
+
this.debugState.child?.kill('SIGKILL');
|
|
3504
|
+
}, 5_000);
|
|
3560
3505
|
this.debugState.promiseOnEnd ||= Promise.resolve(0);
|
|
3561
3506
|
}
|
|
3562
3507
|
else {
|