iobroker.javascript 9.2.4 → 9.3.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 +6 -15
- package/admin/assets/AiChatPanel-C3bIw_D5.js +317 -0
- package/admin/assets/{ScriptEditor-CmEeMfC2.js → ScriptEditor-8UU88tT5.js} +1 -1
- package/admin/assets/{ScriptEditorVanillaMonaco-C22Gpet7.js → ScriptEditorVanillaMonaco-Bn8xJ3LV.js} +2 -2
- package/admin/assets/index-C0ZnUCQf.js +990 -0
- package/admin/assets/{index-BTQOdA7v.js → index-CO9RA-Wq.js} +4 -4
- package/admin/assets/{index-7mrTpzQy.js → index-CRGWWpGb.js} +1 -1
- package/admin/assets/{index-Dggem6AX.js → index-CsyuIWlD.js} +1 -1
- package/admin/assets/localSharedImportMap-Db5DxDmY.js +1 -0
- package/admin/assets/{stateHoverProvider-DxTNxyYN.js → stateHoverProvider-C82xsI7Q.js} +1 -1
- package/admin/assets/{virtual_mf-REMOTE_ENTRY_ID_iobroker_javascript__remoteEntry_js-CeYV4czE.js → virtual_mf-REMOTE_ENTRY_ID_iobroker_javascript__remoteEntry_js-Bw22wl4W.js} +2 -2
- package/admin/custom/assets/index-CMsuyEZj.js +964 -0
- package/admin/custom/assets/{index-B6NrrORQ.js → index-CuAhFQ4L.js} +147 -147
- package/admin/custom/assets/{index-DTbSiEk2.js → index-mkhHeJ3Q.js} +1 -1
- package/admin/custom/assets/localSharedImportMap-BsMINYMr.js +1 -0
- package/admin/custom/assets/{virtual_mf-REMOTE_ENTRY_ID_ConfigCustomJavascriptSet__customComponents_js-ClQrb2n1.js → virtual_mf-REMOTE_ENTRY_ID_ConfigCustomJavascriptSet__customComponents_js-BzLNCazi.js} +2 -2
- package/admin/custom/customComponents.js +1 -1
- package/admin/i18n/de.json +8 -0
- package/admin/i18n/en.json +8 -0
- package/admin/jsonConfig.json +70 -9
- package/admin/mf-manifest.json +1 -1
- package/admin/remoteEntry.js +1 -1
- package/admin/tab.html +2 -2
- package/build/lib/aiProviderResolver.js +32 -5
- package/build/lib/aiProviderResolver.js.map +1 -1
- package/build/lib/sandbox.js +20 -12
- package/build/lib/sandbox.js.map +1 -1
- package/build/main.js +336 -41
- package/build/main.js.map +1 -1
- package/build/types.d.ts +10 -4
- package/io-package.json +34 -28
- package/package.json +9 -9
- package/admin/assets/AiChatPanel-B4vPXgLz.js +0 -315
- package/admin/assets/index-qoSy77Bg.js +0 -990
- package/admin/assets/localSharedImportMap-CVlAGiIu.js +0 -1
- package/admin/custom/assets/index-CT5PwRfY.js +0 -964
- package/admin/custom/assets/localSharedImportMap-ChjFDKsq.js +0 -1
package/build/main.js
CHANGED
|
@@ -316,6 +316,14 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
316
316
|
mirror;
|
|
317
317
|
stopCounters = {};
|
|
318
318
|
setStateCountCheckInterval = null;
|
|
319
|
+
/**
|
|
320
|
+
* Decrypted AI API keys cached from the central credential store (manager mode),
|
|
321
|
+
* keyed by credential ID (e.g. `system.credentials.anthropic`). Kept fresh by the
|
|
322
|
+
* subscriptions set up in `subscribeAiCredentials`.
|
|
323
|
+
*/
|
|
324
|
+
aiCredentialCache = new Map();
|
|
325
|
+
/** Unsubscribe callbacks for the AI credential subscriptions (manager mode). */
|
|
326
|
+
aiCredentialUnsubscribers = [];
|
|
319
327
|
globalScript = '';
|
|
320
328
|
/** Generated declarations for global TypeScripts */
|
|
321
329
|
globalDeclarations = '';
|
|
@@ -323,8 +331,11 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
323
331
|
// have access to, because it depends on the compilation order
|
|
324
332
|
knownGlobalDeclarationsByScript = {};
|
|
325
333
|
globalScriptLines = 0;
|
|
334
|
+
/** Running counter to build unique names for ad-hoc scripts started via the "execute" message */
|
|
335
|
+
executeCounter = 0;
|
|
326
336
|
// compiler instance for typescript
|
|
327
337
|
tsServer;
|
|
338
|
+
logCollectors = [];
|
|
328
339
|
ignoreObjectChange = new Set();
|
|
329
340
|
debugState = {
|
|
330
341
|
scriptName: '',
|
|
@@ -761,6 +772,7 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
761
772
|
clearInterval(this.setStateCountCheckInterval);
|
|
762
773
|
this.setStateCountCheckInterval = null;
|
|
763
774
|
}
|
|
775
|
+
await this.unsubscribeAiCredentials();
|
|
764
776
|
await this.stopAllScripts();
|
|
765
777
|
}
|
|
766
778
|
catch (err) {
|
|
@@ -810,6 +822,116 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
810
822
|
}
|
|
811
823
|
await this.main();
|
|
812
824
|
}
|
|
825
|
+
/** Read and decrypt a single AI credential's key from the central store; returns '' (and logs) on error. */
|
|
826
|
+
async readAiCredentialKey(id) {
|
|
827
|
+
if (!adapter_core_1.Credentials?.getCredentials) {
|
|
828
|
+
this.log.warn(`Cannot read AI credential "${id}": Credentials API is only with 7.2 js-controller available`);
|
|
829
|
+
return '';
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
const cred = await adapter_core_1.Credentials.getCredentials(this, id);
|
|
833
|
+
return (cred?.values?.key || '').trim();
|
|
834
|
+
}
|
|
835
|
+
catch (e) {
|
|
836
|
+
this.log.warn(`Cannot read AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
|
|
837
|
+
return '';
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Resolve the API key (and base URL) for an AI provider.
|
|
842
|
+
*
|
|
843
|
+
* In `manual` mode the key comes from the encryptedNative adapter config.
|
|
844
|
+
* In `manager` mode the config only stores the ID of a credential in the central
|
|
845
|
+
* ioBroker credential store (`system.credentials.*`); the actual key is taken from the
|
|
846
|
+
* `aiCredentialCache` (kept fresh by `subscribeAiCredentials`) or, for credentials we are
|
|
847
|
+
* not subscribed to (e.g. a not-yet-saved selection in the settings dialog), read directly.
|
|
848
|
+
*
|
|
849
|
+
* The settings-dialog Test button may pass form values that are not saved yet
|
|
850
|
+
* (`messageApiKey` / `messageCredentialId` / `credentialType`); those win over the stored config.
|
|
851
|
+
*/
|
|
852
|
+
async resolveAiCredentials(provider, opts = {}) {
|
|
853
|
+
const mode = opts.credentialType || this.config.credentialType || 'manual';
|
|
854
|
+
if (mode === 'manager') {
|
|
855
|
+
// The base URL is not a secret and is resolved the same way in both modes.
|
|
856
|
+
const { baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
|
|
857
|
+
const id = (opts.messageCredentialId || (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider)).trim();
|
|
858
|
+
if (!id) {
|
|
859
|
+
return { apiKey: '', baseUrl };
|
|
860
|
+
}
|
|
861
|
+
// Prefer the cached value kept fresh by the credential subscription.
|
|
862
|
+
const cached = this.aiCredentialCache.get(id);
|
|
863
|
+
const apiKey = cached !== undefined ? cached : await this.readAiCredentialKey(id);
|
|
864
|
+
return { apiKey, baseUrl };
|
|
865
|
+
}
|
|
866
|
+
// Manual mode. The Test button sends the current form key (maybe empty) — let it win.
|
|
867
|
+
if (opts.messageApiKey !== undefined) {
|
|
868
|
+
return (0, aiProviderResolver_1.resolveTestCredentials)(this.config, provider, opts.messageApiKey, opts.messageBaseUrl);
|
|
869
|
+
}
|
|
870
|
+
return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* In `manager` mode, subscribe to all configured AI credentials so that edits made in the
|
|
874
|
+
* admin credential manager (Settings → Credentials) are picked up live, without restarting
|
|
875
|
+
* the adapter (the `system.credentials.*` objects are global, not part of the instance config).
|
|
876
|
+
* The decrypted keys are cached and kept fresh by the subscription handlers.
|
|
877
|
+
*/
|
|
878
|
+
async subscribeAiCredentials() {
|
|
879
|
+
// Always start from a clean state (idempotent — also used to re-subscribe).
|
|
880
|
+
await this.unsubscribeAiCredentials();
|
|
881
|
+
if (this.config.credentialType !== 'manager') {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (!adapter_core_1.Credentials?.subscribeCredentials) {
|
|
885
|
+
this.log.warn(`Cannot subscribe AI credential: Credentials API is only with 7.2 js-controller available`);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
// Collect the distinct credential IDs configured across all AI providers.
|
|
889
|
+
const ids = new Set();
|
|
890
|
+
for (const provider of ['openai', 'anthropic', 'gemini', 'deepseek', 'custom']) {
|
|
891
|
+
const id = (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider);
|
|
892
|
+
if (id) {
|
|
893
|
+
ids.add(id);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
for (const id of ids) {
|
|
897
|
+
try {
|
|
898
|
+
const unsubscribe = await adapter_core_1.Credentials.subscribeCredentials(this, id, (changedId, cred) => {
|
|
899
|
+
if (cred) {
|
|
900
|
+
this.aiCredentialCache.set(changedId, (cred.values?.key || '').trim());
|
|
901
|
+
this.log.debug(`AI credential "${changedId}" updated`);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
// The credential was deleted
|
|
905
|
+
this.aiCredentialCache.delete(changedId);
|
|
906
|
+
this.log.debug(`AI credential "${changedId}" was deleted`);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
this.aiCredentialUnsubscribers.push(unsubscribe);
|
|
910
|
+
// Prime the cache with the current value (the handler may only fire on later changes).
|
|
911
|
+
this.aiCredentialCache.set(id, await this.readAiCredentialKey(id));
|
|
912
|
+
}
|
|
913
|
+
catch (e) {
|
|
914
|
+
this.log.warn(`Cannot subscribe to AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (this.aiCredentialUnsubscribers.length) {
|
|
918
|
+
this.log.debug(`Subscribed to ${this.aiCredentialUnsubscribers.length} AI credential(s)`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/** Tear down all AI credential subscriptions and clear the cache. */
|
|
922
|
+
async unsubscribeAiCredentials() {
|
|
923
|
+
const unsubscribers = this.aiCredentialUnsubscribers;
|
|
924
|
+
this.aiCredentialUnsubscribers = [];
|
|
925
|
+
this.aiCredentialCache.clear();
|
|
926
|
+
for (const unsubscribe of unsubscribers) {
|
|
927
|
+
try {
|
|
928
|
+
await unsubscribe();
|
|
929
|
+
}
|
|
930
|
+
catch (e) {
|
|
931
|
+
this.log.warn(`Cannot unsubscribe from AI credential: ${e instanceof Error ? e.message : String(e)}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
813
935
|
onMessage(obj) {
|
|
814
936
|
switch (obj?.command) {
|
|
815
937
|
// process messageTo commands
|
|
@@ -999,23 +1121,28 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
999
1121
|
}
|
|
1000
1122
|
case 'chatCompletion': {
|
|
1001
1123
|
// Proxy chat completion requests to an OpenAI-compatible API endpoint.
|
|
1002
|
-
// API keys are resolved server-side from encryptedNative config
|
|
1003
|
-
// leave the adapter (frontend only sends `provider
|
|
1004
|
-
|
|
1124
|
+
// API keys are resolved server-side from the encryptedNative config or the central
|
|
1125
|
+
// credentials manager — they never leave the adapter (frontend only sends `provider`).
|
|
1126
|
+
void (async () => {
|
|
1127
|
+
if (!obj.callback) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1005
1130
|
const chatModel = (obj.message?.model || '').trim();
|
|
1006
1131
|
const messages = obj.message?.messages;
|
|
1007
1132
|
const tools = obj.message?.tools;
|
|
1008
1133
|
const provider = (obj.message?.provider || 'openai').trim();
|
|
1009
|
-
const { apiKey, baseUrl } =
|
|
1134
|
+
const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
|
|
1135
|
+
messageBaseUrl: obj.message?.baseUrl,
|
|
1136
|
+
});
|
|
1010
1137
|
// Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
|
|
1011
1138
|
if (!apiKey &&
|
|
1012
1139
|
(provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
|
|
1013
1140
|
this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
|
|
1014
|
-
|
|
1141
|
+
return;
|
|
1015
1142
|
}
|
|
1016
1143
|
if (!chatModel || !messages) {
|
|
1017
1144
|
this.sendTo(obj.from, obj.command, { error: 'Model and messages are required' }, obj.callback);
|
|
1018
|
-
|
|
1145
|
+
return;
|
|
1019
1146
|
}
|
|
1020
1147
|
let url;
|
|
1021
1148
|
const chatHeaders = {
|
|
@@ -1070,7 +1197,7 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1070
1197
|
const resolved = resolveRequestModule(url);
|
|
1071
1198
|
if (!resolved) {
|
|
1072
1199
|
this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
|
|
1073
|
-
|
|
1200
|
+
return;
|
|
1074
1201
|
}
|
|
1075
1202
|
const { module: requestModule, isHttps } = resolved;
|
|
1076
1203
|
try {
|
|
@@ -1143,21 +1270,29 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1143
1270
|
catch (error) {
|
|
1144
1271
|
this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
|
|
1145
1272
|
}
|
|
1146
|
-
}
|
|
1273
|
+
})();
|
|
1147
1274
|
break;
|
|
1148
1275
|
}
|
|
1149
1276
|
case 'testApiConnection': {
|
|
1150
1277
|
// Test connection to an OpenAI-compatible API endpoint.
|
|
1151
1278
|
// The settings-dialog Test button sends the current form value as `apiKey`
|
|
1152
1279
|
// (so users can test before saving); otherwise we fall back to the stored key.
|
|
1153
|
-
|
|
1280
|
+
void (async () => {
|
|
1281
|
+
if (!obj.callback) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1154
1284
|
const provider = (obj.message?.provider || 'openai').trim();
|
|
1155
|
-
const { apiKey, baseUrl } =
|
|
1285
|
+
const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
|
|
1286
|
+
messageApiKey: obj.message?.apiKey,
|
|
1287
|
+
messageBaseUrl: obj.message?.baseUrl,
|
|
1288
|
+
messageCredentialId: obj.message?.credentialId,
|
|
1289
|
+
credentialType: obj.message?.credentialType,
|
|
1290
|
+
});
|
|
1156
1291
|
// Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
|
|
1157
1292
|
if (!apiKey &&
|
|
1158
1293
|
(provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
|
|
1159
1294
|
this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
|
|
1160
|
-
|
|
1295
|
+
return;
|
|
1161
1296
|
}
|
|
1162
1297
|
let url;
|
|
1163
1298
|
const testHeaders = {
|
|
@@ -1187,7 +1322,7 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1187
1322
|
const resolved = resolveRequestModule(url);
|
|
1188
1323
|
if (!resolved) {
|
|
1189
1324
|
this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
|
|
1190
|
-
|
|
1325
|
+
return;
|
|
1191
1326
|
}
|
|
1192
1327
|
const { module: requestModule, isHttps } = resolved;
|
|
1193
1328
|
try {
|
|
@@ -1248,7 +1383,7 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1248
1383
|
catch (error) {
|
|
1249
1384
|
this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
|
|
1250
1385
|
}
|
|
1251
|
-
}
|
|
1386
|
+
})();
|
|
1252
1387
|
break;
|
|
1253
1388
|
}
|
|
1254
1389
|
case 'getAvailableAiProviders': {
|
|
@@ -1301,6 +1436,19 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1301
1436
|
}
|
|
1302
1437
|
break;
|
|
1303
1438
|
}
|
|
1439
|
+
case 'execute': {
|
|
1440
|
+
if (obj.callback) {
|
|
1441
|
+
void this.executeScript(obj.message)
|
|
1442
|
+
.then(result => this.sendTo(obj.from, obj.command, result, obj.callback))
|
|
1443
|
+
.catch(err => this.sendTo(obj.from, obj.command, {
|
|
1444
|
+
ok: false,
|
|
1445
|
+
error: `Internal error: ${err}`,
|
|
1446
|
+
logs: [],
|
|
1447
|
+
output: '',
|
|
1448
|
+
}, obj.callback));
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1304
1452
|
}
|
|
1305
1453
|
}
|
|
1306
1454
|
onLog(msg) {
|
|
@@ -1314,6 +1462,14 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1314
1462
|
}
|
|
1315
1463
|
}
|
|
1316
1464
|
}
|
|
1465
|
+
// Special case if some script is executed now with "execute" command, and we see "script.js.__execute_X:" at the beginning
|
|
1466
|
+
if (this.logCollectors.length) {
|
|
1467
|
+
for (const logCollector of this.logCollectors) {
|
|
1468
|
+
if (msg.message.includes(`${logCollector.name}:`)) {
|
|
1469
|
+
logCollector.collector(msg.severity, msg.message);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1317
1473
|
}
|
|
1318
1474
|
logError(scriptName, msg, e, offs) {
|
|
1319
1475
|
const stack = e.stack ? e.stack.toString().split('\n') : e ? e.toString() : '';
|
|
@@ -1419,6 +1575,9 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1419
1575
|
// Store allowSelfSignedCerts on the context, so sandbox HTTP functions can use it
|
|
1420
1576
|
// without setting the global process.env.NODE_TLS_REJECT_UNAUTHORIZED (which affects all adapters in compact mode)
|
|
1421
1577
|
this.context.allowSelfSignedCerts = this.config.allowSelfSignedCerts;
|
|
1578
|
+
// In `manager` credential mode, subscribe to the configured AI credentials so changes in the
|
|
1579
|
+
// central credential store are picked up live (the keys are cached for the AI sendTo handlers).
|
|
1580
|
+
await this.subscribeAiCredentials();
|
|
1422
1581
|
const doc = await this.getObjectViewAsync('script', 'javascript', {});
|
|
1423
1582
|
if (doc?.rows?.length) {
|
|
1424
1583
|
// sort global scripts if configured
|
|
@@ -2194,7 +2353,14 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
2194
2353
|
return false;
|
|
2195
2354
|
}
|
|
2196
2355
|
}
|
|
2197
|
-
execute(script, name, engineType, verbose, debug
|
|
2356
|
+
execute(script, name, engineType, verbose, debug,
|
|
2357
|
+
/**
|
|
2358
|
+
* Optional sink for the "execute" message API. When provided, the script runs in an
|
|
2359
|
+
* ephemeral diagnostic mode: every log line (the script's own `log()`/`console.*` output
|
|
2360
|
+
* AND all verbose internal operations) is forwarded to this collector instead of the
|
|
2361
|
+
* adapter log, and no `scriptProblem` state is written.
|
|
2362
|
+
*/
|
|
2363
|
+
logCollector) {
|
|
2198
2364
|
script.intervals = new Set();
|
|
2199
2365
|
script.timeouts = new Set();
|
|
2200
2366
|
script.schedules = [];
|
|
@@ -2206,12 +2372,14 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
2206
2372
|
script.subscribesFile = {};
|
|
2207
2373
|
script.setStatePerMinuteCounter = 0;
|
|
2208
2374
|
script.setStatePerMinuteProblemCounter = 0;
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2375
|
+
if (!logCollector) {
|
|
2376
|
+
void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
|
|
2377
|
+
val: false,
|
|
2378
|
+
ack: true,
|
|
2379
|
+
expire: 1000,
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
const sandbox = (0, sandbox_1.sandBox)(script, name, verbose, debug, this.context, logCollector);
|
|
2215
2383
|
try {
|
|
2216
2384
|
script.script.runInNewContext(sandbox, {
|
|
2217
2385
|
filename: name,
|
|
@@ -2220,13 +2388,131 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
2220
2388
|
});
|
|
2221
2389
|
}
|
|
2222
2390
|
catch (err) {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2391
|
+
if (logCollector) {
|
|
2392
|
+
const e = err;
|
|
2393
|
+
const stack = (e?.stack ? e.stack.toString() : String(err))
|
|
2394
|
+
.split('\n')
|
|
2395
|
+
.map(line => this.fixLineNo(line))
|
|
2396
|
+
.join('\n');
|
|
2397
|
+
logCollector('error', `Error by run: ${stack}`);
|
|
2398
|
+
}
|
|
2399
|
+
else {
|
|
2400
|
+
void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
|
|
2401
|
+
val: true,
|
|
2402
|
+
ack: true,
|
|
2403
|
+
c: 'execute',
|
|
2404
|
+
});
|
|
2405
|
+
this.logError(name, 'Error by run:', err);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Run an ad-hoc script sent via the `execute` message and return everything it logged.
|
|
2411
|
+
*
|
|
2412
|
+
* The script is compiled (JavaScript or TypeScript), executed with the same sandbox API as a
|
|
2413
|
+
* regular script (verbose by default, so internal operations like setState/subscribe are logged
|
|
2414
|
+
* too), left running for `timeout` ms to collect asynchronous output, and afterwards stopped and
|
|
2415
|
+
* fully cleaned up (timers, subscriptions, schedules). It is ephemeral: no script object or
|
|
2416
|
+
* states are created.
|
|
2417
|
+
*
|
|
2418
|
+
* Expected `message`:
|
|
2419
|
+
* - `source` / `code` (string, required) – the script source
|
|
2420
|
+
* - `engineType` (string, optional) – `TypeScript/ts` to compile as TypeScript, otherwise JavaScript
|
|
2421
|
+
* - `verbose` (boolean, optional, default `true`) – log internal sandbox operations
|
|
2422
|
+
* - `logLevel` (silly|debug|info|warn|error, optional, default `silly`) – minimum severity to return
|
|
2423
|
+
* - `timeout` (number ms, optional, default 5000, clamped to 0…60000) – collection window
|
|
2424
|
+
* - `maxLogs` (number, optional, default 5000) – cap on returned log lines
|
|
2425
|
+
*/
|
|
2426
|
+
async executeScript(message) {
|
|
2427
|
+
const LEVELS = ['silly', 'debug', 'info', 'warn', 'error'];
|
|
2428
|
+
const source = message?.source ?? message?.code;
|
|
2429
|
+
const engineTypeStr = (message?.engineType || '').toString().toLowerCase();
|
|
2430
|
+
const isTypeScript = engineTypeStr.startsWith('typescript') || engineTypeStr === 'ts';
|
|
2431
|
+
const engineType = isTypeScript ? 'TypeScript/ts' : 'Javascript/js';
|
|
2432
|
+
const empty = (error) => ({ ok: false, error, engineType, runtime: 0, truncated: false, logs: [], output: '' });
|
|
2433
|
+
if (typeof source !== 'string' || !source.trim()) {
|
|
2434
|
+
return empty('No source code provided');
|
|
2435
|
+
}
|
|
2436
|
+
if (this.context.debugMode) {
|
|
2437
|
+
return empty('Cannot execute a script while a debug session is active');
|
|
2438
|
+
}
|
|
2439
|
+
let timeout = parseInt(message?.timeout, 10);
|
|
2440
|
+
if (isNaN(timeout)) {
|
|
2441
|
+
timeout = 5000;
|
|
2442
|
+
}
|
|
2443
|
+
timeout = Math.max(0, Math.min(timeout, 60000));
|
|
2444
|
+
const verbose = message?.verbose !== false;
|
|
2445
|
+
const minLevel = message?.logLevel
|
|
2446
|
+
? LEVELS.includes(message?.logLevel)
|
|
2447
|
+
? message.logLevel
|
|
2448
|
+
: 'silly'
|
|
2449
|
+
: 'silly';
|
|
2450
|
+
let maxLogs = parseInt(message?.maxLogs, 10);
|
|
2451
|
+
if (isNaN(maxLogs) || maxLogs <= 0) {
|
|
2452
|
+
maxLogs = 5000;
|
|
2453
|
+
}
|
|
2454
|
+
const name = `${SCRIPT_CODE_MARKER}__execute_${++this.executeCounter}`;
|
|
2455
|
+
// Compile the source the same way regular scripts are compiled
|
|
2456
|
+
let createdScript;
|
|
2457
|
+
if (isTypeScript) {
|
|
2458
|
+
const transformedSource = (0, typescriptTools_1.transformScriptBeforeCompilation)(source, false);
|
|
2459
|
+
const filename = (0, typescriptTools_1.scriptIdToTSFilename)(name);
|
|
2460
|
+
let tsCompiled;
|
|
2461
|
+
try {
|
|
2462
|
+
tsCompiled = this.tsServer.compile(filename, transformedSource);
|
|
2463
|
+
}
|
|
2464
|
+
catch (err) {
|
|
2465
|
+
return empty(`TypeScript compilation failed: ${err}`);
|
|
2466
|
+
}
|
|
2467
|
+
if (!tsCompiled.success) {
|
|
2468
|
+
const errors = tsCompiled.diagnostics.map(diag => diag.annotatedSource).join('\n');
|
|
2469
|
+
return empty(`TypeScript compilation failed:\n${errors}`);
|
|
2470
|
+
}
|
|
2471
|
+
createdScript = this.createVM(`${this.globalScript}\n${tsCompiled.result || ''}`, name, false);
|
|
2472
|
+
}
|
|
2473
|
+
else {
|
|
2474
|
+
createdScript = this.createVM(`${this.globalScript}\n${source}`, name, true);
|
|
2229
2475
|
}
|
|
2476
|
+
if (!createdScript) {
|
|
2477
|
+
return empty('Compilation failed');
|
|
2478
|
+
}
|
|
2479
|
+
const logs = [];
|
|
2480
|
+
let truncated = false;
|
|
2481
|
+
const collector = (severity, msg) => {
|
|
2482
|
+
if (logs.length >= maxLogs) {
|
|
2483
|
+
truncated = true;
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
logs.push({ ts: Date.now(), severity, message: msg });
|
|
2487
|
+
};
|
|
2488
|
+
this.logCollectors.push({ name, collector });
|
|
2489
|
+
this.updateLogSubscriptions();
|
|
2490
|
+
this.scripts[name] = createdScript;
|
|
2491
|
+
this.execute(createdScript, name, engineType, verbose, false, collector);
|
|
2492
|
+
// Let asynchronous output (timeouts, awaited code, triggered subscriptions) accumulate
|
|
2493
|
+
if (timeout) {
|
|
2494
|
+
await new Promise(resolve => setTimeout(resolve, timeout));
|
|
2495
|
+
}
|
|
2496
|
+
// Stop and clean up the ephemeral script (timers, subscriptions, schedules, …)
|
|
2497
|
+
await this.stopScript(name, true);
|
|
2498
|
+
const pos = this.logCollectors.findIndex(it => it.name === name);
|
|
2499
|
+
if (pos !== -1) {
|
|
2500
|
+
this.logCollectors.splice(pos, 1);
|
|
2501
|
+
}
|
|
2502
|
+
this.updateLogSubscriptions();
|
|
2503
|
+
const minIdx = LEVELS.indexOf(minLevel);
|
|
2504
|
+
const filtered = logs.filter(entry => {
|
|
2505
|
+
const idx = LEVELS.indexOf(entry.severity);
|
|
2506
|
+
return idx < 0 || idx >= minIdx;
|
|
2507
|
+
});
|
|
2508
|
+
return {
|
|
2509
|
+
ok: true,
|
|
2510
|
+
engineType,
|
|
2511
|
+
runtime: timeout,
|
|
2512
|
+
truncated,
|
|
2513
|
+
logs: filtered,
|
|
2514
|
+
output: filtered.map(entry => `[${entry.severity}] ${entry.message}`).join('\n'),
|
|
2515
|
+
};
|
|
2230
2516
|
}
|
|
2231
2517
|
/**
|
|
2232
2518
|
* Finds the index of `id` in a sorted array using binary search – O(log n).
|
|
@@ -2302,35 +2588,44 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
2302
2588
|
}
|
|
2303
2589
|
// Analyze if logs are still required or not
|
|
2304
2590
|
updateLogSubscriptions() {
|
|
2305
|
-
let found =
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2591
|
+
let found = '';
|
|
2592
|
+
if (this.logCollectors.length) {
|
|
2593
|
+
found = this.logCollectors[0].name;
|
|
2594
|
+
}
|
|
2595
|
+
else {
|
|
2596
|
+
// go through all scripts and check if some script still requires logs
|
|
2597
|
+
Object.keys(this.logSubscriptions).forEach(scriptName => {
|
|
2598
|
+
if (!this.logSubscriptions?.[scriptName] || !this.logSubscriptions[scriptName].length) {
|
|
2599
|
+
delete this.logSubscriptions[scriptName];
|
|
2600
|
+
}
|
|
2601
|
+
else {
|
|
2602
|
+
found = scriptName;
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2315
2606
|
if (this.requireLog) {
|
|
2316
2607
|
if (found && !this.logSubscribed) {
|
|
2317
2608
|
this.logSubscribed = true;
|
|
2318
2609
|
void this.requireLog(this.logSubscribed);
|
|
2319
|
-
this.log.info(`Subscribed to log messages (found
|
|
2610
|
+
this.log.info(`Subscribed to log messages (at least because of ${found})`);
|
|
2320
2611
|
}
|
|
2321
2612
|
else if (!found && this.logSubscribed) {
|
|
2322
2613
|
this.logSubscribed = false;
|
|
2323
2614
|
void this.requireLog(this.logSubscribed);
|
|
2324
|
-
this.log.info(`Unsubscribed from log messages (not found
|
|
2615
|
+
this.log.info(`Unsubscribed from log messages (not found any subscribers)`);
|
|
2325
2616
|
}
|
|
2326
2617
|
}
|
|
2327
2618
|
}
|
|
2328
|
-
async stopScript(name) {
|
|
2619
|
+
async stopScript(name, silent) {
|
|
2329
2620
|
if (!this.scripts[name]) {
|
|
2330
2621
|
return false;
|
|
2331
2622
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2623
|
+
// `silent` is used for ephemeral scripts started via the "execute" message – they have no
|
|
2624
|
+
// `scriptEnabled` state and should not appear in the adapter log.
|
|
2625
|
+
if (!silent) {
|
|
2626
|
+
this.log.info(`${name}: Stopping script`);
|
|
2627
|
+
await this.setState(`scriptEnabled.${name.substring(SCRIPT_CODE_MARKER.length)}`, false, true);
|
|
2628
|
+
}
|
|
2334
2629
|
if (this.messageBusHandlers[name]) {
|
|
2335
2630
|
delete this.messageBusHandlers[name];
|
|
2336
2631
|
}
|