iobroker.javascript 10.4.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 +3 -13
- package/admin/assets/AiChatPanel-CfjSsVpE.js +27 -0
- package/admin/assets/AiChatService-rFZwYNPk.js +216 -0
- package/admin/assets/AiInlineProvider-R_AVYwFT.js +5 -0
- package/admin/assets/{BlocklyEditor-DhwThchX.js → BlocklyEditor-DPJkoxjP.js} +3 -3
- package/admin/assets/{Compile-e8N5zI-J.js → Compile-BgTrxmPA.js} +1 -1
- package/admin/assets/{Debugger-BDQUKM1K.js → Debugger-CThCzBvP.js} +1 -1
- package/admin/assets/{FbEditor-BjLPNzpK.js → FbEditor-A7I5U0hA.js} +1 -1
- package/admin/assets/{RulesEditor--YX6ETsU.js → RulesEditor-MDkGXERp.js} +1 -1
- package/admin/assets/{ScriptEditor-BadPC8Mf.js → ScriptEditor-CqphehA8.js} +1 -1
- package/admin/assets/{ScriptEditorVanillaMonaco-BnAmy59d.js → ScriptEditorVanillaMonaco-CxgNa-3O.js} +3 -3
- package/admin/assets/aiPromptBuilder-CI2ChET0.js +1 -0
- package/admin/assets/{blocks_action-Ctn5A1AU.js → blocks_action-3CepYoiX.js} +1 -1
- package/admin/assets/{blocks_convert-BI0lf0sf.js → blocks_convert-BEpxVx-_.js} +1 -1
- package/admin/assets/{blocks_logic-o49klUX_.js → blocks_logic-CyIBT53p.js} +1 -1
- package/admin/assets/{blocks_number-DZtyWFyR.js → blocks_number-yqm4qwfh.js} +1 -1
- package/admin/assets/{blocks_object-BuzGCxVr.js → blocks_object-CizLkmWp.js} +1 -1
- package/admin/assets/{blocks_procedures-jn9sIYmR.js → blocks_procedures-pFTThYqz.js} +1 -1
- package/admin/assets/{blocks_sendto-Coui8w1H.js → blocks_sendto-rEq1xPmO.js} +1 -1
- package/admin/assets/{blocks_switch-CeRZizTM.js → blocks_switch-D3aybjqu.js} +1 -1
- package/admin/assets/{blocks_system-CaBPIC40.js → blocks_system-BVRsS2OE.js} +1 -1
- package/admin/assets/{blocks_text-DeDtPYWf.js → blocks_text-D93xQ_03.js} +1 -1
- package/admin/assets/{blocks_time-CTXQH3Lm.js → blocks_time-ChxhjgD7.js} +1 -1
- package/admin/assets/{blocks_timeout-D8IOKOb-.js → blocks_timeout-CUZ80HaP.js} +1 -1
- package/admin/assets/{blocks_trigger-zuuXJbhb.js → blocks_trigger-DivLpAuy.js} +1 -1
- package/admin/assets/{helpers-BwllSRzJ.js → helpers-Bt4VKZBw.js} +1 -1
- package/admin/assets/{index-Bmmscq2G.js → index-DzAQnPTD.js} +5 -5
- package/admin/assets/{loadBlocks-BcFtthDb.js → loadBlocks-DDLPEDa6.js} +1 -1
- package/admin/assets/{mf-entry-bootstrap-0-6db6c4fc.js → mf-entry-bootstrap-0-d055f05d.js} +1 -1
- package/admin/mf-stats.json +1 -1
- package/admin/tab.html +2 -2
- package/build/lib/ai.js +53 -0
- package/build/lib/ai.js.map +1 -0
- package/build/main.js +19 -618
- package/build/main.js.map +1 -1
- package/io-package.json +13 -13
- package/package.json +2 -1
- package/admin/assets/AiChatPanel-BjRIKaXG.js +0 -27
- package/admin/assets/AiChatService-DP1EmVk2.js +0 -216
- package/admin/assets/AiInlineProvider-jkEIJJJ-.js +0 -5
- package/admin/assets/aiPromptBuilder-Co6AqZwx.js +0 -1
- package/build/lib/aiProviderResolver.js +0 -260
- package/build/lib/aiProviderResolver.js.map +0 -1
- package/build/lib/anthropicAdapter.js +0 -212
- package/build/lib/anthropicAdapter.js.map +0 -1
package/build/main.js
CHANGED
|
@@ -80,9 +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
|
|
83
|
+
const ai_core_1 = require("@iobroker/ai-core");
|
|
84
|
+
const ai_1 = require("./lib/ai");
|
|
84
85
|
const scriptHistory_1 = require("./lib/scriptHistory");
|
|
85
|
-
const anthropicAdapter_1 = require("./lib/anthropicAdapter");
|
|
86
86
|
const eventObj_1 = require("./lib/eventObj");
|
|
87
87
|
const scheduler_1 = require("./lib/scheduler");
|
|
88
88
|
const typescriptSettings_1 = require("./lib/typescriptSettings");
|
|
@@ -108,11 +108,6 @@ const forbiddenMirrorLocations = [
|
|
|
108
108
|
];
|
|
109
109
|
const packageJson = JSON.parse((0, node_fs_1.readFileSync)(`${__dirname}/../package.json`).toString());
|
|
110
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';
|
|
116
111
|
let webstormDebug;
|
|
117
112
|
const isCI = !!process.env.CI;
|
|
118
113
|
// ambient declarations for typescript
|
|
@@ -257,33 +252,6 @@ const jsDeclarationServer = new lazyTsServer_1.LazyTsServer(typescriptSettings_1
|
|
|
257
252
|
* Stores the IDs of script objects whose change should be ignored because
|
|
258
253
|
* the compiled source was just updated
|
|
259
254
|
*/
|
|
260
|
-
const HTTP_STATUS_TEXTS = {
|
|
261
|
-
400: 'Bad Request',
|
|
262
|
-
401: 'Unauthorized',
|
|
263
|
-
403: 'Forbidden',
|
|
264
|
-
404: 'Not Found',
|
|
265
|
-
429: 'Too Many Requests / Rate Limit',
|
|
266
|
-
500: 'Internal Server Error',
|
|
267
|
-
502: 'Bad Gateway',
|
|
268
|
-
503: 'Service Unavailable',
|
|
269
|
-
};
|
|
270
|
-
function httpStatusText(code) {
|
|
271
|
-
return HTTP_STATUS_TEXTS[code] ?? `Error ${code}`;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Resolves the correct http/https module based on the URL string.
|
|
275
|
-
* Returns null if the URL is invalid.
|
|
276
|
-
*/
|
|
277
|
-
function resolveRequestModule(url) {
|
|
278
|
-
try {
|
|
279
|
-
const { protocol } = new URL(url);
|
|
280
|
-
const isHttps = protocol === 'https:';
|
|
281
|
-
return { module: isHttps ? https : http, isHttps };
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
255
|
class JavaScript extends adapter_core_1.Adapter {
|
|
288
256
|
context;
|
|
289
257
|
errorLogFunction = {
|
|
@@ -346,21 +314,11 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
346
314
|
mirror;
|
|
347
315
|
stopCounters = {};
|
|
348
316
|
setStateCountCheckInterval = null;
|
|
349
|
-
/**
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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();
|
|
362
|
-
/** Unsubscribe callbacks for the AI credential subscriptions (manager mode). */
|
|
363
|
-
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
|
+
});
|
|
364
322
|
/**
|
|
365
323
|
* Decrypted credentials of the central ioBroker credential store (`system.credentials.*`),
|
|
366
324
|
* exposed to the scripts as the global `SECRETS` object. Kept up to date by `onObjectChange`.
|
|
@@ -412,8 +370,8 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
412
370
|
* with nothing in any log. So the editor gets an immediate acknowledgement and the result
|
|
413
371
|
* later as an instance message.
|
|
414
372
|
*/
|
|
415
|
-
uiClientSubscribe: (info) => this.onUiClientSubscribe(info),
|
|
416
|
-
uiClientUnsubscribe: (info) => this.onUiClientUnsubscribe(info),
|
|
373
|
+
uiClientSubscribe: (info) => this.ai.onUiClientSubscribe(info) ?? { accepted: false, error: 'Unknown subscription type' },
|
|
374
|
+
uiClientUnsubscribe: (info) => this.ai.onUiClientUnsubscribe(info),
|
|
417
375
|
/**
|
|
418
376
|
* If the JS-Controller catches an unhandled error, this will be called,
|
|
419
377
|
* so we have a chance to handle it ourselves.
|
|
@@ -1077,7 +1035,7 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1077
1035
|
clearInterval(this.setStateCountCheckInterval);
|
|
1078
1036
|
this.setStateCountCheckInterval = null;
|
|
1079
1037
|
}
|
|
1080
|
-
await this.
|
|
1038
|
+
await this.ai.stop();
|
|
1081
1039
|
this.secretsManager.destroy();
|
|
1082
1040
|
if (this.editorCheckInterval) {
|
|
1083
1041
|
clearInterval(this.editorCheckInterval);
|
|
@@ -1134,166 +1092,6 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1134
1092
|
}
|
|
1135
1093
|
await this.main();
|
|
1136
1094
|
}
|
|
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
|
-
}
|
|
1210
|
-
/** Read and decrypt a single AI credential's key from the central store; returns '' (and logs) on error. */
|
|
1211
|
-
async readAiCredentialKey(id) {
|
|
1212
|
-
if (!adapter_core_1.Credentials?.getCredentials) {
|
|
1213
|
-
this.log.warn(`Cannot read AI credential "${id}": Credentials API is only with 7.2 js-controller available`);
|
|
1214
|
-
return '';
|
|
1215
|
-
}
|
|
1216
|
-
try {
|
|
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
|
-
}
|
|
1227
|
-
return (cred?.values?.key || '').trim();
|
|
1228
|
-
}
|
|
1229
|
-
catch (e) {
|
|
1230
|
-
this.log.warn(`Cannot read AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
|
|
1231
|
-
return '';
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
/**
|
|
1235
|
-
* Resolve the API key (and base URL) for an AI provider.
|
|
1236
|
-
*
|
|
1237
|
-
* In `manual` mode the key comes from the encryptedNative adapter config.
|
|
1238
|
-
* In `manager` mode the config only stores the ID of a credential in the central
|
|
1239
|
-
* ioBroker credential store (`system.credentials.*`); the actual key is taken from the
|
|
1240
|
-
* `aiCredentialCache` (kept fresh by `subscribeAiCredentials`) or, for credentials we are
|
|
1241
|
-
* not subscribed to (e.g. a not-yet-saved selection in the settings dialog), read directly.
|
|
1242
|
-
*
|
|
1243
|
-
* The settings-dialog Test button may pass form values that are not saved yet
|
|
1244
|
-
* (`messageApiKey` / `messageCredentialId` / `credentialType`); those win over the stored config.
|
|
1245
|
-
*/
|
|
1246
|
-
async resolveAiCredentials(provider) {
|
|
1247
|
-
const mode = this.config.credentialType || 'manual';
|
|
1248
|
-
if (mode === 'manager') {
|
|
1249
|
-
// The base URL is not a secret and is resolved the same way in both modes.
|
|
1250
|
-
const { baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider);
|
|
1251
|
-
const id = (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider).trim();
|
|
1252
|
-
if (!id) {
|
|
1253
|
-
return { apiKey: '', baseUrl };
|
|
1254
|
-
}
|
|
1255
|
-
// Prefer the cached value kept fresh by the credential subscription.
|
|
1256
|
-
const cached = this.aiCredentialCache.get(id);
|
|
1257
|
-
const apiKey = cached !== undefined ? cached : await this.readAiCredentialKey(id);
|
|
1258
|
-
return { apiKey, baseUrl };
|
|
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
|
-
}
|
|
1291
|
-
// Manual mode. The Test button sends the current form key (maybe empty) — let it win.
|
|
1292
|
-
if (opts.messageApiKey !== undefined) {
|
|
1293
|
-
return (0, aiProviderResolver_1.resolveTestCredentials)(this.config, provider, opts.messageApiKey, opts.messageBaseUrl);
|
|
1294
|
-
}
|
|
1295
|
-
return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider);
|
|
1296
|
-
}
|
|
1297
1095
|
/**
|
|
1298
1096
|
* Whether the user a message was sent on behalf of may have code executed here.
|
|
1299
1097
|
*
|
|
@@ -1340,81 +1138,19 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1340
1138
|
if (!(this.config.gptBaseUrl || '').trim()) {
|
|
1341
1139
|
return;
|
|
1342
1140
|
}
|
|
1343
|
-
const
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
: (this.config.gptBaseUrlKey || '').trim();
|
|
1347
|
-
const openai = manager ? (0, aiProviderResolver_1.getProviderCredentialId)(this.config, 'openai') : (this.config.gptKey || '').trim();
|
|
1348
|
-
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) {
|
|
1349
1144
|
this.log.warn(`A custom AI endpoint ("${this.config.gptBaseUrl}") is configured, but its own API key is empty. ` +
|
|
1350
1145
|
`Requests to it are no longer signed with the OpenAI key - move the key of that endpoint into ` +
|
|
1351
1146
|
`the "Custom API key" field of the adapter settings.`);
|
|
1352
1147
|
}
|
|
1353
1148
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
* the adapter (the `system.credentials.*` objects are global, not part of the instance config).
|
|
1358
|
-
* The decrypted keys are cached and kept fresh by the subscription handlers.
|
|
1359
|
-
*/
|
|
1360
|
-
async subscribeAiCredentials() {
|
|
1361
|
-
// Always start from a clean state (idempotent — also used to re-subscribe).
|
|
1362
|
-
await this.unsubscribeAiCredentials();
|
|
1363
|
-
if (this.config.credentialType !== 'manager') {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
if (!adapter_core_1.Credentials?.subscribeCredentials) {
|
|
1367
|
-
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)) {
|
|
1368
1152
|
return;
|
|
1369
1153
|
}
|
|
1370
|
-
// Collect the distinct credential IDs configured across all AI providers.
|
|
1371
|
-
const ids = new Set();
|
|
1372
|
-
for (const provider of ['openai', 'anthropic', 'gemini', 'deepseek', 'custom']) {
|
|
1373
|
-
const id = (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider);
|
|
1374
|
-
if (id) {
|
|
1375
|
-
ids.add(id);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
for (const id of ids) {
|
|
1379
|
-
try {
|
|
1380
|
-
const unsubscribe = await adapter_core_1.Credentials.subscribeCredentials(this, id, (changedId, cred) => {
|
|
1381
|
-
if (cred) {
|
|
1382
|
-
this.aiCredentialCache.set(changedId, (cred.values?.key || '').trim());
|
|
1383
|
-
this.log.debug(`AI credential "${changedId}" updated`);
|
|
1384
|
-
}
|
|
1385
|
-
else {
|
|
1386
|
-
// The credential was deleted
|
|
1387
|
-
this.aiCredentialCache.delete(changedId);
|
|
1388
|
-
this.log.debug(`AI credential "${changedId}" was deleted`);
|
|
1389
|
-
}
|
|
1390
|
-
});
|
|
1391
|
-
this.aiCredentialUnsubscribers.push(unsubscribe);
|
|
1392
|
-
// Prime the cache with the current value (the handler may only fire on later changes).
|
|
1393
|
-
this.aiCredentialCache.set(id, await this.readAiCredentialKey(id));
|
|
1394
|
-
}
|
|
1395
|
-
catch (e) {
|
|
1396
|
-
this.log.warn(`Cannot subscribe to AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
if (this.aiCredentialUnsubscribers.length) {
|
|
1400
|
-
this.log.debug(`Subscribed to ${this.aiCredentialUnsubscribers.length} AI credential(s)`);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
/** Tear down all AI credential subscriptions and clear the cache. */
|
|
1404
|
-
async unsubscribeAiCredentials() {
|
|
1405
|
-
const unsubscribers = this.aiCredentialUnsubscribers;
|
|
1406
|
-
this.aiCredentialUnsubscribers = [];
|
|
1407
|
-
this.aiCredentialCache.clear();
|
|
1408
|
-
for (const unsubscribe of unsubscribers) {
|
|
1409
|
-
try {
|
|
1410
|
-
await unsubscribe();
|
|
1411
|
-
}
|
|
1412
|
-
catch (e) {
|
|
1413
|
-
this.log.warn(`Cannot unsubscribe from AI credential: ${e instanceof Error ? e.message : String(e)}`);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
onMessage(obj) {
|
|
1418
1154
|
switch (obj?.command) {
|
|
1419
1155
|
// process messageTo commands
|
|
1420
1156
|
case 'toScript':
|
|
@@ -1758,341 +1494,6 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
1758
1494
|
}
|
|
1759
1495
|
break;
|
|
1760
1496
|
}
|
|
1761
|
-
case 'chatCompletion': {
|
|
1762
|
-
// Proxy chat completion requests to an OpenAI-compatible API endpoint.
|
|
1763
|
-
// API keys are resolved server-side from the encryptedNative config or the central
|
|
1764
|
-
// credentials manager — they never leave the adapter (frontend only sends `provider`).
|
|
1765
|
-
void (async () => {
|
|
1766
|
-
if (!obj.callback) {
|
|
1767
|
-
this.log.warn(`chatCompletion from ${obj.from} came without a callback - nobody can receive the answer`);
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
const chatModel = (obj.message?.model || '').trim();
|
|
1771
|
-
const messages = obj.message?.messages;
|
|
1772
|
-
const tools = obj.message?.tools;
|
|
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);
|
|
1781
|
-
// How long the caller is willing to wait - see `resolveRequestTimeout`
|
|
1782
|
-
const requestTimeout = (0, aiProviderResolver_1.resolveRequestTimeout)(obj.message?.timeout);
|
|
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);
|
|
1786
|
-
// Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
|
|
1787
|
-
if (!apiKey &&
|
|
1788
|
-
(provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
|
|
1789
|
-
respond({ error: 'No API key provided' });
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
if (!chatModel || !messages) {
|
|
1793
|
-
respond({ error: 'Model and messages are required' });
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
let url;
|
|
1797
|
-
const chatHeaders = {
|
|
1798
|
-
'Content-Type': 'application/json',
|
|
1799
|
-
};
|
|
1800
|
-
let bodyObj;
|
|
1801
|
-
if (provider === 'anthropic') {
|
|
1802
|
-
url = 'https://api.anthropic.com/v1/messages';
|
|
1803
|
-
chatHeaders['x-api-key'] = apiKey;
|
|
1804
|
-
chatHeaders['anthropic-version'] = '2023-06-01';
|
|
1805
|
-
// Translate OpenAI-format messages/tools into Anthropic's content-block format.
|
|
1806
|
-
const { system: systemText, messages: anthropicMessages } = (0, anthropicAdapter_1.translateMessagesToAnthropic)(messages);
|
|
1807
|
-
const anthropicTools = tools?.length ? (0, anthropicAdapter_1.translateToolsToAnthropic)(tools) : [];
|
|
1808
|
-
bodyObj = {
|
|
1809
|
-
model: chatModel,
|
|
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,
|
|
1819
|
-
stream: false,
|
|
1820
|
-
...(systemText ? { system: systemText } : {}),
|
|
1821
|
-
messages: anthropicMessages,
|
|
1822
|
-
...(anthropicTools.length ? { tools: anthropicTools } : {}),
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
else if (provider === 'gemini') {
|
|
1826
|
-
url = 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions';
|
|
1827
|
-
if (apiKey) {
|
|
1828
|
-
chatHeaders.Authorization = `Bearer ${apiKey}`;
|
|
1829
|
-
}
|
|
1830
|
-
bodyObj = { model: chatModel, messages, stream: false, ...(tools?.length ? { tools } : {}) };
|
|
1831
|
-
}
|
|
1832
|
-
else if (provider === 'deepseek') {
|
|
1833
|
-
url = 'https://api.deepseek.com/chat/completions';
|
|
1834
|
-
chatHeaders.Authorization = `Bearer ${apiKey}`;
|
|
1835
|
-
bodyObj = { model: chatModel, messages, stream: false, ...(tools?.length ? { tools } : {}) };
|
|
1836
|
-
}
|
|
1837
|
-
else {
|
|
1838
|
-
url = `${baseUrl || 'https://api.openai.com/v1'}/chat/completions`;
|
|
1839
|
-
if (apiKey) {
|
|
1840
|
-
chatHeaders.Authorization = `Bearer ${apiKey}`;
|
|
1841
|
-
}
|
|
1842
|
-
bodyObj = {
|
|
1843
|
-
model: chatModel,
|
|
1844
|
-
messages,
|
|
1845
|
-
stream: false,
|
|
1846
|
-
...(tools?.length ? { tools } : {}),
|
|
1847
|
-
/*
|
|
1848
|
-
* `reasoning_effort` used to be pinned to `none` for every custom base URL,
|
|
1849
|
-
* to save context and time on a local model. Behind a proxy that fronts a
|
|
1850
|
-
* subscription, the same setting turns off the reasoning the model is being
|
|
1851
|
-
* used for, or is rejected outright. It is a setting now, and the default is
|
|
1852
|
-
* to leave the parameter out and let the endpoint decide.
|
|
1853
|
-
*/
|
|
1854
|
-
...(this.config.aiReasoningEffort
|
|
1855
|
-
? { reasoning_effort: this.config.aiReasoningEffort }
|
|
1856
|
-
: {}),
|
|
1857
|
-
};
|
|
1858
|
-
}
|
|
1859
|
-
const body = JSON.stringify(bodyObj);
|
|
1860
|
-
const bodyBuffer = Buffer.from(body, 'utf8');
|
|
1861
|
-
chatHeaders['Content-Length'] = bodyBuffer.length;
|
|
1862
|
-
const resolved = resolveRequestModule(url);
|
|
1863
|
-
if (!resolved) {
|
|
1864
|
-
respond({ error: `Invalid API URL: ${url}` });
|
|
1865
|
-
return;
|
|
1866
|
-
}
|
|
1867
|
-
const { module: requestModule, isHttps } = resolved;
|
|
1868
|
-
try {
|
|
1869
|
-
const req = requestModule.request(url, {
|
|
1870
|
-
method: 'POST',
|
|
1871
|
-
headers: chatHeaders,
|
|
1872
|
-
timeout: requestTimeout,
|
|
1873
|
-
...(isHttps && this.config.allowSelfSignedCerts ? { rejectUnauthorized: false } : {}),
|
|
1874
|
-
}, res => {
|
|
1875
|
-
let data = '';
|
|
1876
|
-
res.on('data', (chunk) => {
|
|
1877
|
-
data += chunk.toString();
|
|
1878
|
-
});
|
|
1879
|
-
res.on('end', () => {
|
|
1880
|
-
if (res.statusCode === 200) {
|
|
1881
|
-
try {
|
|
1882
|
-
const parsed = JSON.parse(data);
|
|
1883
|
-
let content;
|
|
1884
|
-
let tool_calls;
|
|
1885
|
-
if (provider === 'anthropic') {
|
|
1886
|
-
const translated = (0, anthropicAdapter_1.translateAnthropicResponseToOpenAI)(parsed);
|
|
1887
|
-
content = translated.content;
|
|
1888
|
-
tool_calls = translated.tool_calls;
|
|
1889
|
-
}
|
|
1890
|
-
else {
|
|
1891
|
-
const message = parsed.choices?.[0]?.message;
|
|
1892
|
-
content = message?.content || '';
|
|
1893
|
-
tool_calls = message?.tool_calls;
|
|
1894
|
-
}
|
|
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 });
|
|
1914
|
-
}
|
|
1915
|
-
else {
|
|
1916
|
-
this.log.debug(`chatCompletion (${provider}/${chatModel}): answered with ${content.length} characters, ${tool_calls?.length || 0} tool calls, stop reason ${info.finishReason || '-'}`);
|
|
1917
|
-
respond({
|
|
1918
|
-
success: true,
|
|
1919
|
-
content,
|
|
1920
|
-
...(tool_calls ? { tool_calls } : {}),
|
|
1921
|
-
...info,
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
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' });
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
else {
|
|
1931
|
-
let detail = '';
|
|
1932
|
-
try {
|
|
1933
|
-
const errParsed = JSON.parse(data);
|
|
1934
|
-
detail = errParsed.error?.message || data.substring(0, 200);
|
|
1935
|
-
}
|
|
1936
|
-
catch {
|
|
1937
|
-
detail = data.substring(0, 200);
|
|
1938
|
-
}
|
|
1939
|
-
const error = `${detail || httpStatusText(res.statusCode || 0)} (${res.statusCode})`;
|
|
1940
|
-
this.log.warn(`chatCompletion (${provider}/${chatModel}): ${error}`);
|
|
1941
|
-
respond({ error });
|
|
1942
|
-
}
|
|
1943
|
-
});
|
|
1944
|
-
});
|
|
1945
|
-
req.on('error', (err) => {
|
|
1946
|
-
this.log.warn(`chatCompletion (${provider}/${chatModel}): cannot reach ${url}: ${err.message}`);
|
|
1947
|
-
respond({ error: `Connection failed: ${err.message}` });
|
|
1948
|
-
});
|
|
1949
|
-
req.on('timeout', () => {
|
|
1950
|
-
req.destroy();
|
|
1951
|
-
const error = `Connection timeout (${Math.round(requestTimeout / 1000)}s)`;
|
|
1952
|
-
this.log.warn(`chatCompletion (${provider}/${chatModel}): ${url} - ${error}`);
|
|
1953
|
-
respond({ error });
|
|
1954
|
-
});
|
|
1955
|
-
req.write(bodyBuffer);
|
|
1956
|
-
req.end();
|
|
1957
|
-
}
|
|
1958
|
-
catch (error) {
|
|
1959
|
-
this.log.warn(`chatCompletion (${provider}/${chatModel}): ${url} - ${error.toString()}`);
|
|
1960
|
-
respond({ error: `Connection failed: ${error.toString()}` });
|
|
1961
|
-
}
|
|
1962
|
-
})();
|
|
1963
|
-
break;
|
|
1964
|
-
}
|
|
1965
|
-
case 'testApiConnection': {
|
|
1966
|
-
// Test connection to an OpenAI-compatible API endpoint.
|
|
1967
|
-
// The settings-dialog Test button sends the current form value as `apiKey`
|
|
1968
|
-
// (so users can test before saving); otherwise we fall back to the stored key.
|
|
1969
|
-
void (async () => {
|
|
1970
|
-
if (!obj.callback) {
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
const provider = (obj.message?.provider || 'openai').trim();
|
|
1974
|
-
const { apiKey, baseUrl } = await this.resolveAiTestCredentials(provider, {
|
|
1975
|
-
messageApiKey: obj.message?.apiKey,
|
|
1976
|
-
messageBaseUrl: obj.message?.baseUrl,
|
|
1977
|
-
messageCredentialId: obj.message?.credentialId,
|
|
1978
|
-
credentialType: obj.message?.credentialType,
|
|
1979
|
-
});
|
|
1980
|
-
// Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
|
|
1981
|
-
if (!apiKey &&
|
|
1982
|
-
(provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
|
|
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);
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
let url;
|
|
1997
|
-
const testHeaders = {
|
|
1998
|
-
'Content-Type': 'application/json',
|
|
1999
|
-
};
|
|
2000
|
-
if (provider === 'anthropic') {
|
|
2001
|
-
url = 'https://api.anthropic.com/v1/models';
|
|
2002
|
-
testHeaders['x-api-key'] = apiKey;
|
|
2003
|
-
testHeaders['anthropic-version'] = '2023-06-01';
|
|
2004
|
-
}
|
|
2005
|
-
else if (provider === 'gemini') {
|
|
2006
|
-
url = 'https://generativelanguage.googleapis.com/v1beta/openai/models';
|
|
2007
|
-
if (apiKey) {
|
|
2008
|
-
testHeaders.Authorization = `Bearer ${apiKey}`;
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
else if (provider === 'deepseek') {
|
|
2012
|
-
url = 'https://api.deepseek.com/models';
|
|
2013
|
-
testHeaders.Authorization = `Bearer ${apiKey}`;
|
|
2014
|
-
}
|
|
2015
|
-
else {
|
|
2016
|
-
url = `${baseUrl || 'https://api.openai.com/v1'}/models`;
|
|
2017
|
-
if (apiKey) {
|
|
2018
|
-
testHeaders.Authorization = `Bearer ${apiKey}`;
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
const resolved = resolveRequestModule(url);
|
|
2022
|
-
if (!resolved) {
|
|
2023
|
-
this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
const { module: requestModule, isHttps } = resolved;
|
|
2027
|
-
try {
|
|
2028
|
-
const req = requestModule.request(url, {
|
|
2029
|
-
method: 'GET',
|
|
2030
|
-
headers: testHeaders,
|
|
2031
|
-
timeout: 10000,
|
|
2032
|
-
...(isHttps && this.config.allowSelfSignedCerts ? { rejectUnauthorized: false } : {}),
|
|
2033
|
-
}, res => {
|
|
2034
|
-
let data = '';
|
|
2035
|
-
res.on('data', (chunk) => {
|
|
2036
|
-
data += chunk.toString();
|
|
2037
|
-
});
|
|
2038
|
-
res.on('end', () => {
|
|
2039
|
-
if (res.statusCode === 200) {
|
|
2040
|
-
try {
|
|
2041
|
-
const parsed = JSON.parse(data);
|
|
2042
|
-
const models = (parsed.data || [])
|
|
2043
|
-
.map((m) => m.id.startsWith('models/') ? m.id.substring(7) : m.id)
|
|
2044
|
-
.sort();
|
|
2045
|
-
this.sendTo(obj.from, obj.command, { success: true, models, count: models.length }, obj.callback);
|
|
2046
|
-
}
|
|
2047
|
-
catch {
|
|
2048
|
-
this.sendTo(obj.from, obj.command, { error: 'Invalid JSON response from API' }, obj.callback);
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
else if (res.statusCode === 401) {
|
|
2052
|
-
this.sendTo(obj.from, obj.command, { error: 'Invalid API key (401)' }, obj.callback);
|
|
2053
|
-
}
|
|
2054
|
-
else if (res.statusCode === 403) {
|
|
2055
|
-
this.sendTo(obj.from, obj.command, { error: 'Access denied (403)' }, obj.callback);
|
|
2056
|
-
}
|
|
2057
|
-
else {
|
|
2058
|
-
// Include response body for debugging
|
|
2059
|
-
let detail = '';
|
|
2060
|
-
try {
|
|
2061
|
-
const errParsed = JSON.parse(data);
|
|
2062
|
-
detail = errParsed.error?.message || data.substring(0, 200);
|
|
2063
|
-
}
|
|
2064
|
-
catch {
|
|
2065
|
-
detail = data.substring(0, 200);
|
|
2066
|
-
}
|
|
2067
|
-
this.sendTo(obj.from, obj.command, {
|
|
2068
|
-
error: `${detail || httpStatusText(res.statusCode || 0)} (${res.statusCode})`,
|
|
2069
|
-
}, obj.callback);
|
|
2070
|
-
}
|
|
2071
|
-
});
|
|
2072
|
-
});
|
|
2073
|
-
req.on('error', (err) => {
|
|
2074
|
-
this.sendTo(obj.from, obj.command, { error: `Connection failed: ${err.message}` }, obj.callback);
|
|
2075
|
-
});
|
|
2076
|
-
req.on('timeout', () => {
|
|
2077
|
-
req.destroy();
|
|
2078
|
-
this.sendTo(obj.from, obj.command, { error: 'Connection timeout (10s)' }, obj.callback);
|
|
2079
|
-
});
|
|
2080
|
-
req.end();
|
|
2081
|
-
}
|
|
2082
|
-
catch (error) {
|
|
2083
|
-
this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
|
|
2084
|
-
}
|
|
2085
|
-
})();
|
|
2086
|
-
break;
|
|
2087
|
-
}
|
|
2088
|
-
case 'getAvailableAiProviders': {
|
|
2089
|
-
// Reports which AI providers have stored credentials (keys never leave the backend).
|
|
2090
|
-
if (obj.callback) {
|
|
2091
|
-
const providers = (0, aiProviderResolver_1.listAvailableProviders)(this.config);
|
|
2092
|
-
this.sendTo(obj.from, obj.command, { providers }, obj.callback);
|
|
2093
|
-
}
|
|
2094
|
-
break;
|
|
2095
|
-
}
|
|
2096
1497
|
case 'getSecrets': {
|
|
2097
1498
|
// Reports which credentials exist and which fields they have, so the instance
|
|
2098
1499
|
// settings and the Blockly editor can show the available `SECRETS.<name>.<field>`
|
|
@@ -2312,9 +1713,9 @@ class JavaScript extends adapter_core_1.Adapter {
|
|
|
2312
1713
|
// Store allowSelfSignedCerts on the context, so sandbox HTTP functions can use it
|
|
2313
1714
|
// without setting the global process.env.NODE_TLS_REJECT_UNAUTHORIZED (which affects all adapters in compact mode)
|
|
2314
1715
|
this.context.allowSelfSignedCerts = this.config.allowSelfSignedCerts;
|
|
2315
|
-
// In `manager` credential mode
|
|
2316
|
-
// central credential store
|
|
2317
|
-
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();
|
|
2318
1719
|
this.warnAboutMisplacedCustomAiKey();
|
|
2319
1720
|
// Read the central credential store, so the scripts can use `SECRETS.<name>.<field>`.
|
|
2320
1721
|
// Later changes are picked up in `onObjectChange`.
|