iobroker.eos-admin 7.9.92 → 7.9.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/INSTALL_TEST_V7.9.93.md +12 -0
  2. package/NEXOWATT_EOS_BUILD_INFO.json +18 -19
  3. package/{PUBLISH_STABLE_V7.9.92.md → PUBLISH_STABLE_V7.9.93.md} +4 -4
  4. package/README_STABILITY_V7.9.93.md +15 -0
  5. package/RELEASE_ACCEPTANCE_V7.9.93.md +15 -0
  6. package/RELEASE_NOTES_V7.9.93.md +10 -0
  7. package/adminWww/assets/Adapters-B5_jQ7DE-v84.js +1 -1
  8. package/adminWww/css/nexowatt-native-shell.css +154 -2
  9. package/adminWww/index.html +11 -11
  10. package/adminWww/js/eos-account-management.js +1 -1
  11. package/adminWww/js/eos-assistant.js +487 -55
  12. package/adminWww/js/eos-basic-settings.js +1 -1
  13. package/adminWww/js/eos-branding-sanitizer.js +3 -1
  14. package/adminWww/js/eos-native-security.js +1 -1
  15. package/adminWww/js/eos-policy-client.js +1 -1
  16. package/adminWww/js/eos-role-bootstrap.js +2 -2
  17. package/adminWww/js/eos-role-ui.js +1 -1
  18. package/adminWww/js/nexowatt-native-shell.js +1 -1
  19. package/build/lib/chat/assistantKnowledge.js +21 -6
  20. package/build/lib/chat/chatOrchestrator.js +33 -38
  21. package/build/lib/chat/mcpClientManager.js +1 -1
  22. package/build/main.js +307 -1
  23. package/io-package.json +13 -8
  24. package/package.json +9 -9
  25. package/tools/nexowatt-account-management-selftest.cjs +1 -1
  26. package/tools/nexowatt-internal-reserve-selftest.cjs +3 -1
  27. package/tools/nexowatt-login-layout-selftest.cjs +19 -38
  28. package/tools/nexowatt-modern-ui-selftest.cjs +1 -1
  29. package/tools/nexowatt-validate-package.cjs +5 -5
  30. package/INSTALL_TEST_V7.9.92.md +0 -18
  31. package/README_STABILITY_V7.9.92.md +0 -14
  32. package/RELEASE_ACCEPTANCE_V7.9.92.md +0 -12
  33. package/RELEASE_NOTES_V7.9.92.md +0 -17
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v91-final-first-login-submit-guard';
4
+ const VERSION = 'v93-final-first-login-submit-guard';
5
5
  const script = document.currentScript || document.querySelector('script[src*="eos-role-bootstrap.js"]');
6
6
  const entry = script?.dataset?.eosEntry || '';
7
7
  let launched = false;
@@ -500,7 +500,7 @@
500
500
  const { username, password, card, submit } = loginElements();
501
501
  if (!username || !password || !card) return;
502
502
  card.classList.add('eos-login-card-modern');
503
- // v91 compact normal-login first activation: do not add role buttons or a second panel.
503
+ // v93 compact normal-login first activation: do not add role buttons or a second panel.
504
504
  // The account is entered in the existing Loginname field; a blank password is accepted only
505
505
  // after the backend confirms that this exact managed account is eligible.
506
506
  card.querySelectorAll('.eos-login-role-selector').forEach(element => element.remove());
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v91-final-clickable-overview-live-reserve-filter';
4
+ const VERSION = 'v93-final-clickable-overview-live-reserve-filter';
5
5
  window.NEXOWATT_EOS_ROLE_UI_VERSION = VERSION;
6
6
 
7
7
  const state = {
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v91-nexowatt-native-shell-modern-overview';
4
+ const VERSION = 'v93-nexowatt-native-shell-modern-overview';
5
5
  const previous = window.NEXOWATT_NATIVE_SHELL;
6
6
  if (previous?.version === VERSION) return;
7
7
  previous?.destroy?.();
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Curated "wiki" of ioBroker how-to knowledge that is injected into the assistant's system prompt.
3
+ * Curated "wiki" of NexoWatt EOS knowledge that is injected into the assistant's system prompt.
4
4
  *
5
5
  * This is meant to grow over time: add a new {@link KnowledgeSection} to {@link KNOWLEDGE_SECTIONS}
6
6
  * for each task the assistant should know how to guide the user through. Keep entries short and
@@ -10,6 +10,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.KNOWLEDGE_SECTIONS = void 0;
11
11
  exports.renderAssistantKnowledge = renderAssistantKnowledge;
12
12
  exports.KNOWLEDGE_SECTIONS = [
13
+ {
14
+ title: 'NexoWatt EOS product areas and role-aware support',
15
+ body: [
16
+ 'EOS Assist can answer questions about the live NexoWatt EOS system: Overview/Cockpit, Modules,',
17
+ 'Services, Datapoints, Structure (rooms/functions), System logs, Access & Rights, NexoWatt EOS',
18
+ 'applications, System hosts, Files and NexoWatt Backup. It can also inspect installed modules,',
19
+ 'running services, object/state metadata, current values, device assignments, logs and history',
20
+ 'through the available read tools. Always query real system data before making a system-specific claim.',
21
+ 'Respect the authenticated role: Admin/Service may receive full system and service detail;',
22
+ 'Installers may receive commissioning and troubleshooting detail available to their account;',
23
+ 'End users receive only their permitted Smart Home, display and operating information.',
24
+ 'Internal package names and compatibility identifiers are technical details; use NexoWatt EOS product',
25
+ 'terminology in normal answers and show technical IDs only when they are required for a precise action.',
26
+ ].join(' '),
27
+ },
13
28
  {
14
29
  title: 'Open an adapter instance configuration',
15
30
  body: [
@@ -197,7 +212,7 @@ exports.KNOWLEDGE_SECTIONS = [
197
212
  {
198
213
  title: 'Run a script in the javascript adapter (run_javascript)',
199
214
  body: [
200
- 'To run logic WITH the full ioBroker scripting API, use run_javascript (act mode). It sends the',
215
+ 'To run logic WITH the full NexoWatt EOS scripting API, use run_javascript (act mode). It sends the',
201
216
  'code to the `javascript` adapter and runs it ONCE (not saved), returning { ok, error, output,',
202
217
  'logs }. Choose `language` "js" (default) or "ts". Inside the script the usual API is available:',
203
218
  '`getState("id").val`, `setState("id", value)`, `on({id, change:"ne"}, …)`, `schedule(...)`,',
@@ -210,10 +225,10 @@ exports.KNOWLEDGE_SECTIONS = [
210
225
  ].join('\n'),
211
226
  },
212
227
  {
213
- title: 'Automation options in ioBroker — pick the right tool',
228
+ title: 'Automation options in NexoWatt EOS — pick the right tool',
214
229
  body: [
215
- 'ioBroker can automate from simple to fully programmable. IMPORTANT: every option below needs a',
216
- 'SEPARATE adapter that is NOT part of a default ioBroker installation — `scenes`, `scheduler`,',
230
+ 'NexoWatt EOS can automate from simple to fully programmable. IMPORTANT: every option below needs a',
231
+ 'SEPARATE adapter that is NOT part of a default NexoWatt EOS installation — `scenes`, `scheduler`,',
217
232
  '`fullcalendar` and `javascript` (which provides Blockly, JavaScript and TypeScript). Before you',
218
233
  'recommend or use one, CHECK whether it is installed (list_adapters, and list_instances for a',
219
234
  'running instance). If it is missing, tell the user it must be installed first and offer',
@@ -271,7 +286,7 @@ exports.KNOWLEDGE_SECTIONS = [
271
286
  ].join('\n'),
272
287
  },
273
288
  {
274
- title: 'ioBroker JavaScript adapter API (cheat-sheet for scripts and run_javascript)',
289
+ title: 'NexoWatt EOS JavaScript API (cheat-sheet for scripts and run_javascript)',
275
290
  body: [
276
291
  'When you WRITE a script (for the Scripts tab or for run_javascript), use the ioBroker javascript',
277
292
  'API — it is NOT plain Node.js. Key functions:',
@@ -76,53 +76,48 @@ class ChatOrchestrator {
76
76
  this.mcp = mcp;
77
77
  this.adapter = adapter;
78
78
  }
79
- /** System prompt describing the assistant's role, tool usage and the current permission mode. */
80
- buildSystemPrompt(language, mode, uiContext) {
79
+ /** System prompt describing EOS Assist, tool usage and the authenticated role. */
80
+ buildSystemPrompt(language, mode, uiContext, accessRole = 'enduser') {
81
+ const roleText = accessRole === 'admin'
82
+ ? 'Admin / NexoWatt Service with full service visibility'
83
+ : accessRole === 'installer'
84
+ ? 'Installer with commissioning and troubleshooting visibility'
85
+ : 'End user with only explicitly permitted Smart Home and operating visibility';
81
86
  const lines = [
82
- 'You are the ioBroker Admin Assistant, an in-app helper inside the ioBroker admin web UI.',
83
- 'You help the user operate their ioBroker installation: explain how to do things in the UI,',
84
- 'recommend which adapter to use for a device or service, and answer questions about the',
85
- "user's actual system (their states, objects, devices grouped by room/function, installed",
86
- 'adapters, running instances, logs and historical values).',
87
+ 'You are EOS Assist inside NexoWatt EOS.',
88
+ 'Your job is to answer questions about the user\'s ACTUAL EOS system and guide them through the',
89
+ 'NexoWatt EOS interface. You cover all settings and product areas: Cockpit/Overview, Modules,',
90
+ 'Services, Datapoints, Structure (rooms/functions), System logs, Access & Rights, EOS applications,',
91
+ 'System hosts, Files, NexoWatt Backup, licenses, devices, communication protocols and diagnostics.',
87
92
  '',
88
- 'KEY CAPABILITY: you can navigate the admin UI yourself. When the user asks you to OPEN or GO to',
89
- 'something, call the navigate_admin_ui tool with the hash route — it opens the tab OR the specific',
90
- 'dialog immediately (no confirmation, it only changes the view), e.g.',
91
- '{"hash":"#tab-hosts/base-settings/system.host.MSI"}. Otherwise (just pointing the user somewhere)',
92
- 'offer a clickable Markdown hash link, e.g. [Base settings of MSI](#tab-hosts/base-settings/system.host.MSI).',
93
- 'NEVER say a dialog can only be opened manually or that deep-linking a dialog is not possible.',
93
+ `Authenticated role: ${roleText}.`,
94
+ 'Never reveal data or instructions outside that role. The tool layer enforces ACLs; if data is not',
95
+ 'available, state that it is not available to the current account instead of guessing.',
94
96
  '',
95
- 'Guidelines:',
96
- '- Use the available tools to look up real data from THIS ioBroker system instead of guessing.',
97
- ' E.g. to list devices in a room call list_devices with the room name; to read a value call',
98
- ' get_states; to find objects call search_objects.',
99
- '- Base your answer on the tool results. If a tool returns an error or nothing, say so briefly',
100
- ' and suggest what the user could check.',
101
- '- Be concise and practical. Use short lists or tables when listing devices or states.',
102
- '- Object IDs are case-sensitive (e.g. "hue.0.lights.1.on"); show them in `monospace`.',
103
- '- IMPORTANT — this admin version SUPPORTS deep links that open specific DIALOGS directly,',
104
- ' not just tabs. It is FALSE to say a dialog "can only be opened with the gear/cog icon" or',
105
- ' that deep-linking a dialog is "technically not possible" — never say that. Instead always',
106
- ' offer a clickable Markdown hash link and let the USER click it; do NOT claim you opened or',
107
- ' navigated anywhere yourself. Examples: [Instances](#tab-instances), [Objects](#tab-objects),',
108
- ' [hue.0 config](#tab-instances/config/hue.0), [edit this object](#tab-objects/edit/<id>),',
109
- " and a host's base-settings dialog, e.g. MSI:",
110
- ' [Base settings of MSI](#tab-hosts/base-settings/system.host.MSI). The exact dialog routes are',
111
- ' listed in the knowledge below. Prefer these links over the navigate_admin_ui tool.',
112
- `- Answer in the user's language. The admin UI language is currently "${language}".`,
97
+ 'Rules:',
98
+ '- For every system-specific question, use the available read tools and base the answer on their results.',
99
+ '- Never invent installed modules, running services, settings, datapoint values, devices or errors.',
100
+ '- Use NexoWatt EOS product terminology in normal answers. Do not present the underlying platform name',
101
+ ' as the product. Technical package IDs or command names may appear only in `code` when strictly needed.',
102
+ '- Explain results clearly and practically. Use short lists or tables for multiple devices or values.',
103
+ '- Object and state IDs are case-sensitive; show necessary IDs in `monospace`.',
104
+ '- You may navigate the EOS interface with navigate_admin_ui when the user explicitly asks to open a',
105
+ ' page. Use these EOS routes: Cockpit `#tab-intro`, Modules `#tab-adapters`, Services `#tab-instances`,',
106
+ ' Datapoints `#tab-objects`, Structure `#tab-enums`, System logs `#tab-logs`, Access & Rights',
107
+ ' `#tab-users`, and instance configuration `#tab-instances/config/<module>.<instance>`.',
108
+ '- Do not claim an action was completed unless a tool result confirms it.',
109
+ `- Answer in the user\'s language. The current EOS language is "${language}".`,
113
110
  ];
114
111
  if (mode === 'act') {
115
- lines.push('- You may change things using the write/action tools (set_state, create_user_state,', ' extend_object, assign_to_enums, install_adapter, run_command, run_node_script). Every such', ' call is shown to the user for explicit confirmation before it runs, so propose the smallest', ' sensible action.', '- create_user_state only works in "0_userdata.0." or "javascript.<n>." — never elsewhere.', '- extend_object merges common/native into an existing object — use it to start/stop an', ' instance (common.enabled), change instance settings (native) or set enum members.', '- assign_to_enums adds objects to room/function enums (add-only) — use it to sort devices.', '- run_command runs an ioBroker CLI command and streams its output to the user; use it for', ' maintenance actions (restart, upload, fix, ...), not to fetch data.', '- run_node_script executes a short Node.js script on the host to CHECK things (network, files,', ' environment). Prefer read-only checks, keep it short, and put results in console.log. Never', ' use it for destructive operations.', '- run_javascript runs a one-off JS/TS script INSIDE the javascript adapter (full ioBroker API:', ' on/getState/setState/schedule/sendTo). Use it to test automation logic or do an API-based', ' check/action; it is not saved. Use run_node_script instead for plain OS checks (no ioBroker API).');
112
+ lines.push('- Changes are possible only through explicit write/action tools and confirmation. Propose the', ' smallest safe action and respect all EOS safety and role restrictions.');
116
113
  }
117
114
  else {
118
- lines.push('- You currently have READ-ONLY access: you cannot change states, create objects or install', ' adapters. When a change is needed, explain the exact steps the user should take in the UI.');
115
+ lines.push('- This header assistant is READ-ONLY. It may inspect and explain the system, but it must not', ' change states, objects, settings, services, modules, passwords or backups.');
119
116
  }
120
117
  if (uiContext?.hash) {
121
- lines.push('', `The user is currently in the admin UI at the route \`${uiContext.hash}\`. Take this into ` +
122
- "account — don't tell them to open a tab/page they are already on, and refer to where " +
123
- 'they are when helpful.');
118
+ lines.push('', `The user is currently on EOS route \`${uiContext.hash}\`. Consider this context.`);
124
119
  }
125
- lines.push('', 'ioBroker how-to knowledge (use this to guide the user precisely):', (0, assistantKnowledge_1.renderAssistantKnowledge)());
120
+ lines.push('', 'NexoWatt EOS knowledge:', (0, assistantKnowledge_1.renderAssistantKnowledge)());
126
121
  return { role: 'system', content: lines.join('\n') };
127
122
  }
128
123
  /** Assemble the tool list offered to the model for the given mode. */
@@ -154,7 +149,7 @@ class ChatOrchestrator {
154
149
  ? { ...message, content: parseTextToolCalls(message.content).content }
155
150
  : message);
156
151
  if (!messages.some(message => message.role === 'system')) {
157
- messages.unshift(this.buildSystemPrompt(language, mode, params.uiContext));
152
+ messages.unshift(this.buildSystemPrompt(language, mode, params.uiContext, params.accessRole));
158
153
  }
159
154
  const newMessages = [];
160
155
  const steps = [];
@@ -44,7 +44,7 @@ class McpClientManager {
44
44
  language: this.options.language,
45
45
  allowSetState: this.options.allowSetState ?? false,
46
46
  allowObjectChange: this.options.allowObjectChange ?? false,
47
- clientName: 'ioBroker.admin chat helper',
47
+ clientName: 'NexoWatt EOS Assist',
48
48
  clientVersion: '1.0.0',
49
49
  })
50
50
  .then(mcp => {
package/build/main.js CHANGED
@@ -174,8 +174,13 @@ class Admin extends adapter_core_1.Adapter {
174
174
  timerNews;
175
175
  _tasks;
176
176
  changedPasswords = [];
177
- /** In-process ioBroker.mcp connection backing the chat helper. Created on first chat message. */
177
+ /** Legacy in-process chat connection kept for compatibility with existing admin messages. */
178
178
  mcpChat = null;
179
+ /** Role-scoped, read-only MCP connections used by the header EOS Assist. */
180
+ eosAssistMcpChats = new Map();
181
+ /** Per-user throttling and concurrency guard for provider calls. */
182
+ eosAssistLastRequestAt = new Map();
183
+ eosAssistInFlight = new Set();
179
184
  /** Periodic guard that keeps EOS safety policy applied even if object events are missed. */
180
185
  eosSecurityTimer = null;
181
186
  /** Debounce timer for object-change triggered safety checks. */
@@ -316,6 +321,300 @@ class Admin extends adapter_core_1.Adapter {
316
321
  });
317
322
  return this.mcpChat;
318
323
  }
324
+ /** Return a read-only MCP connection whose tool calls run under the authenticated user's ACL. */
325
+ getEosAssistMcp(userId) {
326
+ const key = `${userId}:${systemLanguage}`;
327
+ let manager = this.eosAssistMcpChats.get(key);
328
+ if (!manager) {
329
+ manager = new mcpClientManager_1.McpClientManager(this, {
330
+ defaultUser: userId,
331
+ language: systemLanguage,
332
+ allowSetState: false,
333
+ allowObjectChange: false,
334
+ });
335
+ this.eosAssistMcpChats.set(key, manager);
336
+ }
337
+ return manager;
338
+ }
339
+ normalizeEosAssistSettings(value) {
340
+ const native = value && typeof value === 'object' && !Array.isArray(value)
341
+ ? value
342
+ : {};
343
+ const allowed = ['openai', 'anthropic', 'gemini', 'deepseek', 'custom'];
344
+ const rawProvider = typeof native.provider === 'string' ? native.provider : 'anthropic';
345
+ const provider = allowed.includes(rawProvider) ? rawProvider : 'anthropic';
346
+ return {
347
+ provider,
348
+ model: typeof native.model === 'string' ? native.model.trim().substring(0, 200) : '',
349
+ credentialId: typeof native.credentialId === 'string' ? native.credentialId.trim().substring(0, 300) : '',
350
+ baseUrl: typeof native.baseUrl === 'string' ? native.baseUrl.trim().replace(/\/+$/, '').substring(0, 500) : '',
351
+ allowSelfSignedCerts: native.allowSelfSignedCerts === true,
352
+ };
353
+ }
354
+ isEosAssistConfigured(settings) {
355
+ if (!settings.model) {
356
+ return false;
357
+ }
358
+ return settings.provider === 'custom'
359
+ ? !!(settings.baseUrl || settings.credentialId)
360
+ : !!settings.credentialId;
361
+ }
362
+ async readEosAssistSettings() {
363
+ const object = await this.getForeignObjectAsync('system.ai');
364
+ return this.normalizeEosAssistSettings(object?.native);
365
+ }
366
+ sanitizeEosAssistVisibleText(value) {
367
+ const text = String(value || '');
368
+ // Preserve technical IDs and commands inside code spans while keeping product prose NexoWatt-only.
369
+ return text
370
+ .split(/(```[\s\S]*?```|`[^`\n]*`)/g)
371
+ .map((part, index) => (index % 2 ? part : part.replace(/\bioBroker\b/gi, 'NexoWatt EOS')))
372
+ .join('');
373
+ }
374
+ normalizeEosAssistMessages(value) {
375
+ if (!Array.isArray(value)) {
376
+ return [];
377
+ }
378
+ const messages = [];
379
+ let total = 0;
380
+ for (const entry of value.slice(-24)) {
381
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
382
+ continue;
383
+ }
384
+ const record = entry;
385
+ const role = record.role === 'assistant' ? 'assistant' : record.role === 'user' ? 'user' : null;
386
+ if (!role || typeof record.content !== 'string') {
387
+ continue;
388
+ }
389
+ const content = record.content.trim().substring(0, 6_000);
390
+ if (!content) {
391
+ continue;
392
+ }
393
+ total += content.length;
394
+ if (total > 30_000) {
395
+ break;
396
+ }
397
+ messages.push({ role, content });
398
+ }
399
+ return messages;
400
+ }
401
+ async getEosAssistStatus(socketClient, callback) {
402
+ const userId = socketClient?._acl?.user;
403
+ if (!userId) {
404
+ callback?.({ error: 'Nicht angemeldet.', code: 'notAuthenticated' });
405
+ return;
406
+ }
407
+ const role = this.getEosCachedRole(userId);
408
+ if (this.eosPasswordSetupRequired.has(userId)) {
409
+ callback?.({ error: 'Die persönliche Passwortvergabe muss zuerst abgeschlossen werden.', code: 'passwordSetupRequired' });
410
+ return;
411
+ }
412
+ try {
413
+ const settings = await this.readEosAssistSettings();
414
+ const credentials = role === 'admin' ? await (0, credentials_1.listAiCredentials)(this) : [];
415
+ callback?.({
416
+ success: true,
417
+ user: userId.replace(/^system\.user\./, ''),
418
+ role,
419
+ configured: this.isEosAssistConfigured(settings),
420
+ canConfigure: role === 'admin',
421
+ settings: role === 'admin' ? settings : undefined,
422
+ credentials,
423
+ });
424
+ }
425
+ catch (error) {
426
+ callback?.({ error: error instanceof Error ? error.message : String(error), code: 'statusFailed' });
427
+ }
428
+ }
429
+ async saveEosAssistSettings(socketClient, payload, callback) {
430
+ const userId = socketClient?._acl?.user;
431
+ if (!userId || this.getEosCachedRole(userId) !== 'admin') {
432
+ callback?.({ error: 'Diese Einstellung ist nur für Admin / NexoWatt Service verfügbar.', code: 'permissionDenied' });
433
+ return;
434
+ }
435
+ const settings = this.normalizeEosAssistSettings(payload);
436
+ if (!settings.model) {
437
+ callback?.({ error: 'Bitte ein Modell eintragen.', code: 'invalidSettings' });
438
+ return;
439
+ }
440
+ if (settings.provider === 'custom') {
441
+ if (!settings.baseUrl && !settings.credentialId) {
442
+ callback?.({ error: 'Für einen lokalen/individuellen Anbieter wird eine Basis-URL oder ein Zugang benötigt.', code: 'invalidSettings' });
443
+ return;
444
+ }
445
+ if (settings.baseUrl && !/^https?:\/\//i.test(settings.baseUrl)) {
446
+ callback?.({ error: 'Die Basis-URL muss mit http:// oder https:// beginnen.', code: 'invalidSettings' });
447
+ return;
448
+ }
449
+ }
450
+ else if (!settings.credentialId) {
451
+ callback?.({ error: 'Bitte einen gespeicherten KI-Zugang auswählen.', code: 'invalidSettings' });
452
+ return;
453
+ }
454
+ try {
455
+ const current = await this.getForeignObjectAsync('system.ai');
456
+ const next = {
457
+ ...(current || {}),
458
+ _id: 'system.ai',
459
+ type: 'config',
460
+ common: {
461
+ ...(current?.common || {}),
462
+ name: 'EOS Assist Einstellungen',
463
+ expert: true,
464
+ },
465
+ native: {
466
+ ...(current?.native || {}),
467
+ ...settings,
468
+ },
469
+ };
470
+ await this.setForeignObjectAsync('system.ai', next);
471
+ callback?.({ success: true, configured: true, settings });
472
+ }
473
+ catch (error) {
474
+ callback?.({ error: error instanceof Error ? error.message : String(error), code: 'saveFailed' });
475
+ }
476
+ }
477
+ async testEosAssistSettings(socketClient, payload, callback) {
478
+ const userId = socketClient?._acl?.user;
479
+ if (!userId || this.getEosCachedRole(userId) !== 'admin') {
480
+ callback?.({ error: 'Der Verbindungstest ist nur für Admin / NexoWatt Service verfügbar.', code: 'permissionDenied' });
481
+ return;
482
+ }
483
+ const settings = this.normalizeEosAssistSettings(payload);
484
+ if (settings.provider === 'custom' && settings.baseUrl && !/^https?:\/\//i.test(settings.baseUrl)) {
485
+ callback?.({ error: 'Die Basis-URL muss mit http:// oder https:// beginnen.', code: 'invalidSettings' });
486
+ return;
487
+ }
488
+ try {
489
+ const apiKey = settings.credentialId ? await (0, credentials_1.resolveAiKey)(this, settings.credentialId) : '';
490
+ if (settings.provider !== 'custom' && !apiKey) {
491
+ callback?.({ error: 'Der ausgewählte Zugang enthält keinen verwendbaren API-Schlüssel.', code: 'credentialMissing' });
492
+ return;
493
+ }
494
+ const models = await (0, llmProvider_1.listModels)({
495
+ provider: settings.provider,
496
+ apiKey,
497
+ baseUrl: settings.baseUrl || undefined,
498
+ allowSelfSignedCerts: settings.allowSelfSignedCerts,
499
+ });
500
+ callback?.({ success: true, models, count: models.length });
501
+ }
502
+ catch (error) {
503
+ callback?.({ error: error instanceof Error ? error.message : String(error), code: 'connectionFailed' });
504
+ }
505
+ }
506
+ async processEosAssistSocketRequest(socketClient, payload, callback) {
507
+ const userId = socketClient?._acl?.user;
508
+ if (!userId) {
509
+ callback?.({ error: 'Nicht angemeldet.', code: 'notAuthenticated' });
510
+ return;
511
+ }
512
+ const role = this.getEosCachedRole(userId);
513
+ if (this.eosPasswordSetupRequired.has(userId)) {
514
+ callback?.({ error: 'Die persönliche Passwortvergabe muss zuerst abgeschlossen werden.', code: 'passwordSetupRequired' });
515
+ return;
516
+ }
517
+ const now = Date.now();
518
+ const previous = this.eosAssistLastRequestAt.get(userId) || 0;
519
+ if (now - previous < 1_500) {
520
+ callback?.({ error: 'Bitte einen Moment warten, bevor die nächste Frage gesendet wird.', code: 'rateLimited' });
521
+ return;
522
+ }
523
+ if (this.eosAssistInFlight.has(userId)) {
524
+ callback?.({ error: 'EOS Assist bearbeitet bereits eine Frage.', code: 'busy' });
525
+ return;
526
+ }
527
+ const record = payload && typeof payload === 'object' && !Array.isArray(payload)
528
+ ? payload
529
+ : {};
530
+ const messages = this.normalizeEosAssistMessages(record.messages);
531
+ if (!messages.some(message => message.role === 'user')) {
532
+ callback?.({ error: 'Bitte eine Frage eingeben.', code: 'emptyQuestion' });
533
+ return;
534
+ }
535
+ this.eosAssistLastRequestAt.set(userId, now);
536
+ this.eosAssistInFlight.add(userId);
537
+ try {
538
+ const settings = await this.readEosAssistSettings();
539
+ if (!this.isEosAssistConfigured(settings)) {
540
+ callback?.({
541
+ error: 'EOS Assist ist noch nicht durch Admin / NexoWatt Service konfiguriert.',
542
+ code: 'notConfigured',
543
+ canConfigure: role === 'admin',
544
+ role,
545
+ });
546
+ return;
547
+ }
548
+ const apiKey = settings.credentialId ? await (0, credentials_1.resolveAiKey)(this, settings.credentialId) : '';
549
+ if (settings.provider !== 'custom' && !apiKey) {
550
+ callback?.({
551
+ error: 'Der konfigurierte KI-Zugang enthält keinen verwendbaren API-Schlüssel.',
552
+ code: 'credentialMissing',
553
+ canConfigure: role === 'admin',
554
+ });
555
+ return;
556
+ }
557
+ const orchestrator = new chatOrchestrator_1.ChatOrchestrator(this.getEosAssistMcp(userId), this);
558
+ const uiContextRecord = record.uiContext && typeof record.uiContext === 'object' && !Array.isArray(record.uiContext)
559
+ ? record.uiContext
560
+ : {};
561
+ const result = await orchestrator.run({
562
+ provider: settings.provider,
563
+ model: settings.model,
564
+ apiKey,
565
+ baseUrl: settings.baseUrl || undefined,
566
+ messages,
567
+ language: systemLanguage,
568
+ allowSelfSignedCerts: settings.allowSelfSignedCerts,
569
+ maxToolRounds: 10,
570
+ mode: 'read',
571
+ uiContext: {
572
+ hash: typeof uiContextRecord.hash === 'string' ? uiContextRecord.hash.substring(0, 500) : '',
573
+ },
574
+ accessRole: role,
575
+ });
576
+ const content = this.sanitizeEosAssistVisibleText(result.content);
577
+ const newMessages = result.newMessages
578
+ .filter(message => message.role === 'assistant' && typeof message.content === 'string')
579
+ .map(message => ({ ...message, content: this.sanitizeEosAssistVisibleText(message.content) }));
580
+ callback?.({
581
+ success: true,
582
+ status: 'done',
583
+ content,
584
+ newMessages,
585
+ steps: result.steps.map(step => ({
586
+ ...step,
587
+ result: this.sanitizeEosAssistVisibleText(step.result),
588
+ })),
589
+ clientActions: result.clientActions || [],
590
+ role,
591
+ });
592
+ }
593
+ catch (error) {
594
+ callback?.({
595
+ error: error instanceof Error ? error.message : String(error),
596
+ code: 'assistFailed',
597
+ canConfigure: role === 'admin',
598
+ });
599
+ }
600
+ finally {
601
+ this.eosAssistInFlight.delete(userId);
602
+ }
603
+ }
604
+ installEosAssistSocketCommands(socketAdmin) {
605
+ socketAdmin.addCommandHandler('eosAssistStatus', (socketClient, _payload, callback) => {
606
+ void this.getEosAssistStatus(socketClient, callback);
607
+ });
608
+ socketAdmin.addCommandHandler('eosAssistSaveSettings', (socketClient, payload, callback) => {
609
+ void this.saveEosAssistSettings(socketClient, payload, callback);
610
+ });
611
+ socketAdmin.addCommandHandler('eosAssistTestSettings', (socketClient, payload, callback) => {
612
+ void this.testEosAssistSettings(socketClient, payload, callback);
613
+ });
614
+ socketAdmin.addCommandHandler('eosAssist', (socketClient, payload, callback) => {
615
+ void this.processEosAssistSocketRequest(socketClient, payload, callback);
616
+ });
617
+ }
319
618
  /**
320
619
  * Some message was sent to this instance over the message box. Used by email, pushover, text2speech, ...
321
620
  * Using this method requires the "common.messagebox" property to be set to true in io-package.json
@@ -760,6 +1059,12 @@ class Admin extends adapter_core_1.Adapter {
760
1059
  void this.mcpChat.close();
761
1060
  this.mcpChat = null;
762
1061
  }
1062
+ for (const manager of this.eosAssistMcpChats.values()) {
1063
+ void manager.close();
1064
+ }
1065
+ this.eosAssistMcpChats.clear();
1066
+ this.eosAssistLastRequestAt.clear();
1067
+ this.eosAssistInFlight.clear();
763
1068
  try {
764
1069
  this.log.info(`terminating http${this.config.secure ? 's' : ''} server on port ${this.config.port}`);
765
1070
  socket?.close();
@@ -2052,6 +2357,7 @@ class Admin extends adapter_core_1.Adapter {
2052
2357
  }
2053
2358
  socket = new socket_classes_1.SocketAdmin(settings, this, objects);
2054
2359
  this.installEosSocketCommandGuard(socket);
2360
+ this.installEosAssistSocketCommands(socket);
2055
2361
  socket.start(server, ws_server_1.SocketIO, { store, oauth2Only: true, noBasicAuth: this.config.noBasicAuth });
2056
2362
  // subscribe to all object changes
2057
2363
  socket.subscribe('objectChange', '*');
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "eos-admin",
4
- "version": "7.9.92",
4
+ "version": "7.9.93",
5
5
  "titleLang": {
6
6
  "en": "NexoWatt EOS Admin",
7
7
  "de": "NexoWatt EOS Admin",
@@ -18,6 +18,11 @@
18
18
  "connectionType": "local",
19
19
  "dataSource": "push",
20
20
  "news": {
21
+ "7.9.93": {
22
+ "de": "Testkandidat 7.9.93 entfernt den unnötigen Scrollbalken aus der kompakten Anmeldung, zeigt in der Modulinstallation NexoWatt als sichtbaren Herausgeber und ersetzt die statischen EOS-Assist-Texte durch einen authentifizierten, rollenabhängigen und schreibgeschützten Systemassistenten. EOS Assist kann – nach einmaliger Konfiguration durch Admin/Service – reale Module, Dienste, Einstellungen, Geräte, Datenpunkte, Struktur, Logs, Historie und Sicherungsstatus über die freigegebenen Systemwerkzeuge prüfen. Technische Paketkennungen bleiben intern kompatibel.",
23
+ "en": "Test candidate 7.9.93 removes the unnecessary scrollbar from the compact login, shows NexoWatt as the visible publisher in module installation, and replaces static EOS Assist texts with an authenticated, role-aware, read-only system assistant. Once configured by Admin/Service, EOS Assist can inspect real modules, services, settings, devices, datapoints, structure, logs, history and backup status through the permitted system tools. Technical package identifiers remain internally compatible.",
24
+ "nl": "Testkandidaat 7.9.93 verwijdert de overbodige schuifbalk uit de compacte aanmelding, toont NexoWatt als zichtbare uitgever bij module-installatie en vervangt statische EOS Assist-teksten door een geauthenticeerde, rolafhankelijke en alleen-lezen systeemassistent. Na eenmalige configuratie door Admin/Service kan EOS Assist echte modules, diensten, instellingen, apparaten, datapunten, structuur, logs, historie en back-upstatus controleren via de toegestane systeemtools. Technische pakket-ID’s blijven intern compatibel."
25
+ },
21
26
  "7.9.92": {
22
27
  "de": "Stable 7.9.92 korrigiert die Release-Vorbereitung: Vor Paketprüfung und npm-Veröffentlichung werden die verbindlichen Verkaufsstandardwerte für Authentifizierung, Port 8081 sowie die ausschließlich für Admin/Service sichtbaren internen Admin- und BackItUp-Notfallreserven automatisch normalisiert. Dadurch kann ein gemischter lokaler Arbeitsordner die Sicherheitsprüfung nicht mehr durch einen fehlenden Standardwert abbrechen. Rollen-, Login-, Übersichts- und Dienstefilter aus 7.9.91 bleiben unverändert.",
23
28
  "en": "Stable 7.9.92 hardens release preparation: before package validation and npm publishing, the mandatory sales defaults for authentication, port 8081 and the Admin/Service-only internal admin and BackItUp emergency reserves are normalized automatically. A mixed local working tree can therefore no longer abort the security check merely because a required default value is missing. Role, login, overview and service filtering from 7.9.91 remain unchanged.",
@@ -314,7 +319,7 @@
314
319
  "icon": "admin.svg",
315
320
  "messagebox": true,
316
321
  "enabled": true,
317
- "extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.92/admin/admin.svg",
322
+ "extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.93/admin/admin.svg",
318
323
  "keywords": [
319
324
  "NexoWatt",
320
325
  "EOS",
@@ -325,10 +330,9 @@
325
330
  "licensed"
326
331
  ],
327
332
  "compact": true,
328
- "readme": "https://unpkg.com/iobroker.eos-admin@7.9.92/README.md",
333
+ "readme": "https://unpkg.com/iobroker.eos-admin@7.9.93/README.md",
329
334
  "authors": [
330
- "bluefox <bluefox@ccu.io>",
331
- "hobbyquaker <hq@ccu.io>"
335
+ "NexoWatt"
332
336
  ],
333
337
  "dependencies": [
334
338
  {
@@ -394,9 +398,10 @@
394
398
  "nondeletable": false,
395
399
  "allowAdapterUpdate": true,
396
400
  "allowAdapterDelete": false,
397
- "meta": "https://unpkg.com/iobroker.eos-admin@7.9.92/io-package.json",
401
+ "meta": "https://unpkg.com/iobroker.eos-admin@7.9.93/io-package.json",
398
402
  "npmPackage": "iobroker.eos-admin",
399
- "packetName": "iobroker.eos-admin"
403
+ "packetName": "iobroker.eos-admin",
404
+ "publisher": "NexoWatt"
400
405
  },
401
406
  "native": {
402
407
  "autoUpdate": 24,
@@ -1098,5 +1103,5 @@
1098
1103
  }
1099
1104
  }
1100
1105
  },
1101
- "version": "7.9.92"
1106
+ "version": "7.9.93"
1102
1107
  }