odant-work 1.0.14 → 1.0.16

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 (81) hide show
  1. package/#system/package-manifest.json +54 -46
  2. package/$server/$folder/handlers/methods/create/$handler/class.js +83 -2
  3. package/$server/$folder/lib/icon/icon.js +14 -0
  4. package/$server/$folder/lib/node/node.js +51 -1
  5. package/$server/$folder/lib/tree/tree.js +5 -1
  6. package/.secretsignore +4 -0
  7. package/MODELS/$ai/$folder/$class/$ai/class.js +57 -2
  8. package/MODELS/$ai/$folder/$class/$provider/class.js +1 -1
  9. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/class.js +13 -0
  10. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/provider.js +479 -0
  11. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/trainer.js +3 -3
  12. package/MODELS/GigaChat/$provider/$folder/$class/$ai/class.js +10 -20
  13. package/MODELS/GigaChat/$provider/class.js +10 -7
  14. package/MODELS/GigaChat/GigaChat 2 Lite/$ai/class.js +15 -0
  15. package/MODELS/GigaChat/GigaChat 2 Max/$ai/class.js +16 -0
  16. package/MODELS/GigaChat/GigaChat 2 Pro/$ai/class.js +15 -0
  17. package/MODELS/GigaChat/GigaChat Embeddings/$ai/class.js +13 -0
  18. package/MODELS/Local/$provider/$folder/$class/$ai/class.js +3 -3
  19. package/MODELS/Local/$provider/class.js +7 -3
  20. package/MODELS/MTS-AI/$provider/$folder/$class/$ai/class.js +8 -0
  21. package/MODELS/MTS-AI/$provider/class.js +8 -0
  22. package/MODELS/MTS-AI/Cotype Lite/$ai/class.js +15 -0
  23. package/MODELS/MTS-AI/Cotype Pro 2/$ai/class.js +15 -0
  24. package/MODELS/MTS-AI/Cotype Vision/$ai/class.js +14 -0
  25. package/MODELS/T-Bank/$provider/$folder/$class/$ai/class.js +8 -0
  26. package/MODELS/T-Bank/$provider/class.js +8 -0
  27. package/MODELS/T-Bank/T-Lite 2.0/$ai/class.js +15 -0
  28. package/MODELS/T-Bank/T-Pro 2.0/$ai/class.js +16 -0
  29. package/MODELS/T-Bank/T-VoiceKit ASR/$ai/class.js +13 -0
  30. package/MODELS/T-Bank/T-VoiceKit TTS/$ai/class.js +13 -0
  31. package/MODELS/Yandex/$provider/$folder/$class/$ai/class.js +9 -0
  32. package/MODELS/Yandex/$provider/class.js +9 -0
  33. package/MODELS/Yandex/Alice AI (YandexGPT 5 Pro)/$ai/class.js +15 -0
  34. package/MODELS/Yandex/Yandex Embeddings/$ai/class.js +13 -0
  35. package/MODELS/Yandex/Yandex SpeechKit ASR/$ai/class.js +13 -0
  36. package/MODELS/Yandex/Yandex SpeechKit TTS/$ai/class.js +13 -0
  37. package/MODELS/Yandex/YandexART/$ai/class.js +13 -0
  38. package/MODELS/Yandex/YandexGPT 5 Lite/$ai/class.js +15 -0
  39. package/MODELS/Yandex/YandexGPT Reasoning/$ai/class.js +22 -0
  40. package/oda/tools/icons/lib/svg/ai.svg +20 -0
  41. package/package.json +1 -1
  42. package/scripts/check-ignored-code.mjs +3 -0
  43. package/scripts/check-secrets.mjs +1 -1
  44. package/sources/data/progress.md +17 -0
  45. package/sources/host/auth-methods.js +32 -5
  46. package/sources/host/mail.js +93 -14
  47. package/sources/host/watch.js +13 -2
  48. package/sources/modules/agent/index.js +23 -2
  49. package/sources/modules/trainer/halogen.js +128 -29
  50. package/sources/modules/trainer/huggingface.js +11 -5
  51. package/sources/modules/trainer/index.js +135 -2
  52. package/sources/modules/trainer/providers.js +1213 -0
  53. package/sources/modules/user-profile/user-profile.js +7 -1
  54. package/sources/server/class.js +55 -3
  55. package/MODELS/GigaChat/GigaChat Light/$ai/class.js +0 -15
  56. package/MODELS/GigaChat/GigaChat Pro/$ai/class.js +0 -15
  57. package/MODELS/Halogen/$provider/$folder/$class/$ai/class.js +0 -11
  58. package/MODELS/Halogen/$provider/class.js +0 -16
  59. package/MODELS/Halogen/$provider/readme.md +0 -28
  60. package/MODELS/Halogen/Decider-NPU/$ai/class.js +0 -22
  61. package/MODELS/Halogen/Flux2-Klein-NPU/$ai/class.js +0 -21
  62. package/MODELS/Halogen/Qwen3-ASR-NPU/$ai/class.js +0 -21
  63. package/MODELS/Halogen/Qwen3-Embedding-NPU/$ai/class.js +0 -22
  64. package/MODELS/Halogen/Qwen3-Reranker-NPU/$ai/class.js +0 -21
  65. package/MODELS/Halogen/Qwen3-TTS-NPU/$ai/class.js +0 -26
  66. package/MODELS/Halogen/Qwen3.5-2B-NPU/$ai/class.js +0 -23
  67. package/MODELS/Halogen/Qwen3.8-Flash-Next/$ai/class.js +0 -31
  68. package/MODELS/Local/Flux-Image/$ai/class.js +0 -20
  69. package/MODELS/Local/Qwen3-0.6B/$ai/class.js +0 -26
  70. package/MODELS/Local/Qwen3-14B/$ai/class.js +0 -27
  71. package/MODELS/Local/Qwen3-30B-A3B/$ai/class.js +0 -30
  72. package/MODELS/Local/Qwen3-Embedding/$ai/class.js +0 -21
  73. package/MODELS/Local/Qwen3-Voice/$ai/class.js +0 -26
  74. package/MODELS/odant/$provider/$folder/$class/$ai/class.js +0 -11
  75. package/MODELS/odant/$provider/class.js +0 -75
  76. package/MODELS/odant/$provider/readme.md +0 -21
  77. package/MODELS/odant/Qwen3-ASR/$ai/class.js +0 -15
  78. package/MODELS/odant/Qwen3-TTS/$ai/class.js +0 -22
  79. package/MODELS/odant/Qwen3.8 27b/$ai/class.js +0 -11
  80. package/MODELS/odant/Z-Image-Turbo/$ai/class.js +0 -8
  81. package/MODELS/odant/Z-Image-Turbo/$ai/readme.md +0 -24
@@ -0,0 +1,8 @@
1
+ export default {
2
+ "icon": "ai:mts",
3
+ "label": "MTS AI (Cotype)",
4
+ "form": "provider",
5
+ "protocol": "openai",
6
+ "baseUrl": "https://api.mts.ai/v1/chat/completions",
7
+ "apiKey": "secret:mts.json"
8
+ };
@@ -0,0 +1,15 @@
1
+ export default {
2
+ "label": "Cotype Lite",
3
+ "model": "cotype-lite",
4
+ "icon": "ai:mts",
5
+ "modality": "llm",
6
+ "maxTokens": 32768,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "tools",
13
+ "functions"
14
+ ]
15
+ };
@@ -0,0 +1,15 @@
1
+ export default {
2
+ "label": "Cotype Pro 2",
3
+ "model": "cotype-pro-2",
4
+ "icon": "ai:mts",
5
+ "modality": "llm",
6
+ "maxTokens": 131072,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "tools",
13
+ "functions"
14
+ ]
15
+ };
@@ -0,0 +1,14 @@
1
+ export default {
2
+ "label": "Cotype Vision",
3
+ "model": "cotype-vl",
4
+ "icon": "ai:mts",
5
+ "modality": "vlm",
6
+ "maxTokens": 32768,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "vision"
13
+ ]
14
+ };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ "icon": "ai:tbank",
3
+ "description": "Модель провайдера T-Bank AI (T-Pro)",
4
+ "protocol": "openai",
5
+ "baseUrl": "https://api.tbank.ru/v1/chat/completions",
6
+ "functionCalling": true,
7
+ "apiKey": "secret:tbank.json"
8
+ };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ "icon": "ai:tbank",
3
+ "label": "T-Bank AI (T-Pro)",
4
+ "form": "provider",
5
+ "protocol": "openai",
6
+ "baseUrl": "https://api.tbank.ru/v1/chat/completions",
7
+ "apiKey": "secret:tbank.json"
8
+ };
@@ -0,0 +1,15 @@
1
+ export default {
2
+ "label": "T-Lite 2.0",
3
+ "model": "t-tech/T-lite-it-2.0",
4
+ "icon": "ai:tbank",
5
+ "modality": "llm",
6
+ "maxTokens": 131072,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "tools",
13
+ "functions"
14
+ ]
15
+ };
@@ -0,0 +1,16 @@
1
+ export default {
2
+ "label": "T-Pro 2.0",
3
+ "model": "t-tech/T-pro-it-2.0",
4
+ "icon": "ai:tbank",
5
+ "modality": "llm",
6
+ "maxTokens": 131072,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "tools",
13
+ "functions",
14
+ "thinking"
15
+ ]
16
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ "label": "T-VoiceKit ASR",
3
+ "model": "t-voicekit-asr",
4
+ "icon": "carbon:microphone",
5
+ "modality": "audio",
6
+ "maxTokens": 4096,
7
+ "maxOutput": 2048,
8
+ "functionCalling": false,
9
+ "capabilities": [
10
+ "stt",
11
+ "asr"
12
+ ]
13
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ "label": "T-VoiceKit TTS",
3
+ "model": "t-voicekit-tts",
4
+ "icon": "carbon:volume-up",
5
+ "modality": "audio",
6
+ "maxTokens": 4096,
7
+ "maxOutput": 2048,
8
+ "functionCalling": false,
9
+ "capabilities": [
10
+ "tts",
11
+ "voice"
12
+ ]
13
+ };
@@ -0,0 +1,9 @@
1
+ export default {
2
+ "icon": "ai:yandex",
3
+ "description": "Модель провайдера Yandex AI (Алиса)",
4
+ "protocol": "yandex",
5
+ "baseUrl": "https://llm.api.cloud.yandex.net/v1/chat/completions",
6
+ "functionCalling": true,
7
+ "apiKey": "secret:yandex.json",
8
+ "folderId": "secret:yandex.json"
9
+ };
@@ -0,0 +1,9 @@
1
+ export default {
2
+ "icon": "ai:yandex",
3
+ "label": "Yandex AI (Алиса)",
4
+ "form": "provider",
5
+ "protocol": "yandex",
6
+ "baseUrl": "https://llm.api.cloud.yandex.net/v1/chat/completions",
7
+ "apiKey": "secret:yandex.json",
8
+ "folderId": "secret:yandex.json"
9
+ };
@@ -0,0 +1,15 @@
1
+ export default {
2
+ "label": "Alice AI (YandexGPT 5 Pro)",
3
+ "model": "yandexgpt/latest",
4
+ "icon": "ai:yandex",
5
+ "modality": "llm",
6
+ "maxTokens": 32768,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "tools",
13
+ "functions"
14
+ ]
15
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ "label": "Yandex Embeddings",
3
+ "model": "text-search-doc/latest",
4
+ "icon": "carbon:search-locate",
5
+ "modality": "embed",
6
+ "maxTokens": 4096,
7
+ "maxOutput": 1024,
8
+ "functionCalling": false,
9
+ "capabilities": [
10
+ "embeddings",
11
+ "rag"
12
+ ]
13
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ "label": "Yandex SpeechKit ASR",
3
+ "model": "general:rc",
4
+ "icon": "carbon:microphone",
5
+ "modality": "audio",
6
+ "maxTokens": 4096,
7
+ "maxOutput": 2048,
8
+ "functionCalling": false,
9
+ "capabilities": [
10
+ "stt",
11
+ "asr"
12
+ ]
13
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ "label": "Yandex SpeechKit TTS",
3
+ "model": "alena",
4
+ "icon": "carbon:volume-up",
5
+ "modality": "audio",
6
+ "maxTokens": 4096,
7
+ "maxOutput": 2048,
8
+ "functionCalling": false,
9
+ "capabilities": [
10
+ "tts",
11
+ "voice"
12
+ ]
13
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ "label": "YandexART",
3
+ "model": "yandex-art/latest",
4
+ "icon": "ai:stablediffusion",
5
+ "modality": "image",
6
+ "maxTokens": 4096,
7
+ "maxOutput": 1024,
8
+ "functionCalling": false,
9
+ "capabilities": [
10
+ "image",
11
+ "text-to-image"
12
+ ]
13
+ };
@@ -0,0 +1,15 @@
1
+ export default {
2
+ "label": "YandexGPT 5 Lite",
3
+ "model": "yandexgpt-lite/latest",
4
+ "icon": "ai:yandex",
5
+ "modality": "llm",
6
+ "maxTokens": 32768,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "tools",
13
+ "functions"
14
+ ]
15
+ };
@@ -0,0 +1,22 @@
1
+ export default {
2
+ "label": "YandexGPT Reasoning",
3
+ "model": "yandexgpt/rc",
4
+ "icon": "ai:yandex",
5
+ "modality": "llm",
6
+ "maxTokens": 32768,
7
+ "maxOutput": 8192,
8
+ "functionCalling": true,
9
+ "capabilities": [
10
+ "chat",
11
+ "stream",
12
+ "tools",
13
+ "functions",
14
+ "effort",
15
+ "thinking"
16
+ ],
17
+ "efforts": [
18
+ "low",
19
+ "medium",
20
+ "high"
21
+ ]
22
+ };
@@ -209,4 +209,24 @@
209
209
  <symbol id="characterai">
210
210
  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/>
211
211
  </symbol>
212
+
213
+ <!-- Yandex AI / Алиса (Яндекс) -->
214
+ <symbol id="yandex">
215
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm3.2 15.5h-2.1v-4.3h-1.1c-1.5 0-2.3.8-2.9 2.3l-.8 2H6.1l1.1-2.5c.6-1.3 1.3-2 2.3-2.3-1.5-.4-2.5-1.6-2.5-3.4 0-2.3 1.6-3.8 4.3-3.8h3.9v12zm-2.1-6.1V7.3h-1.6c-1.5 0-2.3.7-2.3 2s.8 2.1 2.3 2.1h1.6z"/>
216
+ </symbol>
217
+
218
+ <!-- T-Bank AI (T-Pro) -->
219
+ <symbol id="tbank">
220
+ <path d="M4 3h16a1 1 0 0 1 1 1v9c0 4.5-3.8 7.8-9 9-5.2-1.2-9-4.5-9-9V4a1 1 0 0 1 1-1zm3 4v2.5h3.7V17h2.6V9.5H17V7H7z"/>
221
+ </symbol>
222
+
223
+ <!-- MTS AI (Cotype) -->
224
+ <symbol id="mts">
225
+ <path d="M12 2C7.5 2 4 7.2 4 13c0 5 3.5 9 8 9s8-4 8-9c0-5.8-3.5-11-8-11zm0 3.2c2.9 0 5.2 4 5.2 7.8 0 3.4-2.3 6-5.2 6s-5.2-2.6-5.2-6c0-3.8 2.3-7.8 5.2-7.8z"/>
226
+ </symbol>
227
+
228
+ <!-- OpenRouter -->
229
+ <symbol id="openrouter">
230
+ <path d="M12 2L3 7v10l9 5 9-5V7l-9-5zm0 2.3l6.8 3.8L12 11.9 5.2 8.1 12 4.3zM5 9.8l6 3.4v6.5l-6-3.3V9.8zm8 9.9v-6.5l6-3.4v6.6l-6 3.3z"/>
231
+ </symbol>
212
232
  </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odant-work",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "ODANT-WORK — платформа цифровой организации (встроенная система учёта, документооборот, управление процессами и приватный мультимодальный ИИ)",
5
5
  "type": "module",
6
6
  "main": "sources/work.js",
@@ -69,7 +69,10 @@ if (ignored.length) {
69
69
 
70
70
  // Проверяем также .npmignore через npm pack --dry-run --json (кроме tests и license-server)
71
71
  const NPM_CODE_ROOTS = new Set(['$server', 'sources', 'oda', 'scripts', 'rules', 'MODELS', 'NODES', 'SERVICES']);
72
+ const NPM_INTENTIONAL_EXCLUDE = /^MODELS\/(?:odant|Halogen)\/|^MODELS\/Local\/(?!\$provider\/)/;
72
73
  const npmCandidates = candidates.filter(f => {
74
+ if (NPM_INTENTIONAL_EXCLUDE.test(f))
75
+ return false;
73
76
  const top = f.split('/')[0];
74
77
  return NPM_CODE_ROOTS.has(top) || !f.includes('/');
75
78
  });
@@ -84,7 +84,7 @@ export function scanText(content, ignores = []) {
84
84
  continue;
85
85
  const value = m[0];
86
86
  if (rule === 'keyword-value' || rule === 'bearer-basic-b64') {
87
- const v = (value.match(/['"]([^'"]+)['"]/) || value.match(/[A-Za-z0-9+/]{20,}/) || [])[1] || value;
87
+ const v = (value.match(/['"]([^'"]+)['"]\s*$/) || value.match(/['"]([^'"]+)['"]/) || value.match(/[A-Za-z0-9+/]{20,}/) || [])[1] || value;
88
88
  if (SAFE_VALUE.test(v) || SAFE_MARK.test(v) || SAFE_MARK.test(line))
89
89
  continue;
90
90
  }
@@ -1,6 +1,23 @@
1
1
  # Прогресс: sources/data
2
2
 
3
3
  ## Последние изменения
4
+ - [20:55, 13.11] Архитектура провайдеров и моделей ИИ в `/MODELS`: чистота дистрибутива (исключён `odant`, пустой единый `/MODELS/Local` вместе с контуром машинок Halo), предустановленные российские провайдеры (`GigaChat`, `Yandex`, `T-Bank`, `MTS-AI`), быстрое добавление любых внешних провайдеров и цветовая индикация активности моделей в дереве (`sources/modules/trainer/{providers,halogen,huggingface,index}.js`, `MODELS/$ai/**`, `MODELS/{GigaChat,Yandex,T-Bank,MTS-AI,Local}/**`, `$server/$folder/handlers/methods/create/$handler/class.js`, `$server/$folder/lib/{node/node,icon/icon,tree/tree}.js`, `sources/server/class.js`, `.npmignore`, `odant-work@1.0.16`):
5
+ - **Чистота `/MODELS` в поставке и объединение `Local` + `Halogen`:**
6
+ - Внутренний провайдер разработчика `/MODELS/odant` исключён из npm-дистрибутива (`.npmignore`: `/MODELS/odant/`), а `defaultModel()` в `sources/modules/agent/index.js` автоматически переключается на первую доступную активную модель в `/MODELS`.
7
+ - Ветки `Local` и `Halogen` объединены в единый провайдер **`/MODELS/Local`**, который в чистой поставке полностью пуст — дочерние узлы моделей (`$ai`) создаются в `/MODELS/Local/<Модель>` только при реальной установке/загрузке весов (включая матрицу из 8 GPU+NPU моделей для машинок Halo на базе AMD Ryzen™ AI Max+ PRO 495 192 ГБ).
8
+ - **Предустановленные российские провайдеры ИИ без зашитых ключей:**
9
+ - **`/MODELS/GigaChat` (Сбер):** `GigaChat 2 Lite`, `GigaChat 2 Pro`, `GigaChat 2 Max`, `GigaChat Embeddings`.
10
+ - **`/MODELS/Yandex` (Яндекс AI / Алиса, протокол `yandex`):** `Alice AI (YandexGPT 5 Pro)`, `YandexGPT 5 Lite`, `YandexGPT Reasoning`, `YandexART`, `Yandex SpeechKit ASR`, `Yandex SpeechKit TTS`, `Yandex Embeddings`.
11
+ - **`/MODELS/T-Bank` (Т-Банк AI / T-Pro):** `T-Pro 2.0`, `T-Lite 2.0`, `T-VoiceKit ASR`, `T-VoiceKit TTS`.
12
+ - **`/MODELS/MTS-AI` (МТС AI / Cotype):** `Cotype Pro 2`, `Cotype Lite`, `Cotype Vision`.
13
+ - **Мастер быстрого добавления провайдеров и экран `form/provider`:**
14
+ - При нажатии `+` на `/MODELS` открывается витрина шаблонов мировых и российских провайдеров (`OpenAI`, `Anthropic`, `DeepSeek`, `Gemini`, `OpenRouter`, `Grok`, `Mistral`, `Ollama`, `GigaChat`, `Yandex`, `T-Bank`, `MTS-AI`, `Custom`) с вводом API-ключа и автосозданием набора моделей в 1 клик.
15
+ - Добавлено представление **`form/provider` («Подключение и модели»)** на узлах `$provider` для сохранения ключей в `#secret`, проверки связи с API (`GET /v1/models`), включения/выключения и добавления моделей в 1 клик.
16
+ - **Визуальная индикация активных и неактивных моделей в дереве (`aiActive`):**
17
+ - Активные провайдеры и модели (у которых настроен API-ключ в `#secret` или скачаны локальные веса в `#weights`) помечаются в дереве зелёным индикатором `.ai-dot.active` и полной яркостью, а неактивные — приглушённым серым цветом (`.ai-inactive`, `opacity: 0.48`) с подсказкой причины и скрываются из выпадающего селектора моделей чата.
18
+ - [20:05, 12.11] Прямая 1-клик регистрация первого администратора (`BOSS`) без настроенного почтового сервера и горячее перечитывание SMTP-настроек из `WORK → Почта` (`$server/#secret/email.json`) и `#system/mail.json` (`sources/host/mail.js`, `sources/host/auth-methods.js`, `sources/modules/user-profile/user-profile.js`, `tests/license-and-deploy.test.js`, `odant-work@1.0.15`):
19
+ - **Регистрация первого пользователя (`BOSS`) без SMTP:** если на новом компьютере почтовый сервер ещё не настроен (`getMailer() === null`), а в `/USERS` ещё нет зарегистрированного `BOSS`, `user_register_start` возвращает `{ autoCode }`, и `user-profile.js` сразу в 1 клик генерирует криптоключ, регистрирует пользователя как `BOSS` всего `WORK` и выполняет вход без требования почтового кода.
20
+ - **Настройка почтовика из интерфейса WORK и горячий подхват (`sources/host/mail.js`):** `getMailer()` динамически (по `mtime` без перезапуска сервера) читает настройки SMTP как из `#system/mail.json`, так и из штатного диалога **«Почтовые ящики» (`⚙️` на форме `WORK → Почта`, файл `$server/#secret/email.json`)**, а при отсутствии файлов не засоряет консоль ошибкой `ENOENT`.
4
21
  - [19:30, 12.11] Шаблон организации `BASE_TEMPLATE` с 3 уровнями наполнения (`1. Пустая`, `2. С типовой конфигурацией`, `3. С демоданными`), полный типовой план счетов и операций в `/MODULES/Register`, чистое развёртывание `/ORGANIZATIONS` в npm и защита кастомизированных классов при многошаговых обновлениях (`sources/server/class/base-template.js`, `sources/server/class.js`, `$server/$folder/handlers/methods/create/$handler/class.js`, `MODULES/Register/**`, `.npmignore`, `run.mjs`, `sources/host/auto-updater.js`, `tests/license-and-deploy.test.js`, `odant-work@1.0.14`):
5
22
  - **1. Чистый `/ORGANIZATIONS` в npm-пакете и шаблон `BASE_TEMPLATE` (`sources/server/class/base-template.js`, `create/$handler/class.js`):**
6
23
  - Из npm-поставки исключены все локальные организации (`/ORGANIZATIONS/*/` кроме системной метапапки `/ORGANIZATIONS/$domain/`).
@@ -1,7 +1,7 @@
1
1
  import * as crypto from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
3
  import { DEV_MODE, HOST, CHALLENGE_TTL_MS } from './config.js';
4
- import { mailer } from './mail.js';
4
+ import { getMailer } from './mail.js';
5
5
  import * as CORE from '../server/index.js';
6
6
  import { $server } from '../server/server.js';
7
7
  import { audit } from '../server/access/audit.js';
@@ -56,6 +56,25 @@ function mcpUser(params = {}) {
56
56
  return s.uid;
57
57
  }
58
58
 
59
+ /**
60
+ * Проверить, является ли регистрируемый пользователь первым администратором (BOSS) на сервере
61
+ * (в /USERS ещё нет ни одного зарегистрированного BOSS) или уже действующим BOSS сервера.
62
+ */
63
+ async function isFirstOrBossUser(uid) {
64
+ try {
65
+ const $users = await this.$users;
66
+ const users = await $users.items;
67
+ await WORK.init;
68
+ const existingUserIds = new Set((users || []).map(u => u?.id).filter(Boolean));
69
+ const currentBosses = Array.isArray(WORK?.DATA?.['#security']?.BOSS) ? WORK.DATA['#security'].BOSS : [];
70
+ const isFirstUser = !users?.length || !currentBosses.some(id => existingUserIds.has(id));
71
+ return isFirstUser || (Boolean(uid) && currentBosses.includes(uid));
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+
59
78
  export const authMethods = {
60
79
  async user_register_start(params = {}) {
61
80
  let { uid, email } = params.post || {};
@@ -73,13 +92,21 @@ export const authMethods = {
73
92
  if (DEV_MODE) {
74
93
  console.log('[DEV] Registration code for', email, ':', code);
75
94
  }
76
- if (!mailer) {
95
+ const activeMailer = getMailer();
96
+ if (!activeMailer) {
97
+ const firstOrBoss = await isFirstOrBossUser.call(this, uid);
98
+ if (firstOrBoss && !DEV_MODE) {
99
+ return {
100
+ autoCode: code,
101
+ message: 'Регистрация первого администратора (BOSS) выполняется напрямую без почтового сервера.',
102
+ };
103
+ }
77
104
  if (DEV_MODE) {
78
105
  return "На Вашу почту отправлено письмо с одноразовым кодом, введите его для продолжения регистрации.";
79
106
  }
80
- throw new Error('Почтовый сервер не настроен (#system/mail.json)');
107
+ throw new Error('Почтовый сервер не настроен. Администратор (BOSS) может настроить почтовый ящик в корне WORK → представление «Почта» (⚙️ Почтовые ящики) или в файле #system/mail.json');
81
108
  }
82
- const fromUser = mailer?.options?.auth?.user || 'noreply@localhost';
109
+ const fromUser = activeMailer.fromUser || activeMailer.transport?.options?.auth?.user || 'noreply@localhost';
83
110
  const mailOptions = {
84
111
  from: `"ODANT-WORK" <${fromUser}>`,
85
112
  to: email,
@@ -96,7 +123,7 @@ export const authMethods = {
96
123
  <p>С уважением,<br>WORK</p>
97
124
  `,
98
125
  };
99
- await mailer.sendMail(mailOptions);
126
+ await activeMailer.transport.sendMail(mailOptions);
100
127
  return "На Вашу почту отправлено письмо с одноразовым кодом, введите его для продолжения регистрации.";
101
128
  },
102
129
 
@@ -1,14 +1,93 @@
1
- import * as fs from 'node:fs';
2
- import nodemailer from 'nodemailer';
3
-
4
- export const mailer = (() => {
5
- try {
6
- let data = fs.readFileSync('./#system/mail.json', { encoding: 'utf-8' });
7
- data = JSON.parse(data);
8
- return nodemailer.createTransport(data);
9
- }
10
- catch (e) {
11
- console.error(e);
12
- return null;
13
- }
14
- })();
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import nodemailer from 'nodemailer';
4
+
5
+ const BOOT_DIR = path.resolve('.');
6
+ const REL_CANDIDATES = [
7
+ '#system/mail.json',
8
+ '$server/#secret/email.json',
9
+ '#system/email.json',
10
+ ];
11
+
12
+ let cachedKey = '';
13
+ let cachedTransport = null;
14
+ let cachedFromUser = '';
15
+
16
+ function extractSmtpConfig(raw) {
17
+ if (!raw || typeof raw !== 'object')
18
+ return null;
19
+ // 1. Прямой формат #system/mail.json ({ host, port, secure, auth: { user, pass } })
20
+ if (typeof raw.host === 'string' && raw.host.trim()) {
21
+ return {
22
+ transportOpts: raw,
23
+ fromUser: raw.auth?.user || raw.from || 'noreply@localhost',
24
+ };
25
+ }
26
+ // 2. Формат Почты WORK ($server/#secret/email.json: { mailboxes: { addr: { smtp, auth } } })
27
+ if (raw.mailboxes && typeof raw.mailboxes === 'object') {
28
+ for (const [addr, box] of Object.entries(raw.mailboxes)) {
29
+ const host = String(box?.smtp?.host || '').trim();
30
+ if (!host)
31
+ continue;
32
+ const user = String(box?.auth?.user || addr || '').trim();
33
+ const pass = String(box?.auth?.pass || '');
34
+ return {
35
+ transportOpts: {
36
+ host,
37
+ port: Number(box.smtp.port) || 465,
38
+ secure: box.smtp.secure !== false,
39
+ auth: user ? { user, pass } : undefined,
40
+ },
41
+ fromUser: user || addr || 'noreply@localhost',
42
+ };
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Динамически получить настроенный SMTP-транспорт (перечитывает #system/mail.json
50
+ * или почтовый ящик корня WORK из $server/#secret/email.json при изменении файла без рестарта сервера).
51
+ */
52
+ export function getMailer() {
53
+ if (process.env.WORK_NO_MAIL === '1')
54
+ return null;
55
+
56
+ const cwd = path.resolve('.');
57
+ const roots = cwd === BOOT_DIR ? [cwd] : [cwd, BOOT_DIR];
58
+
59
+ for (const root of roots) {
60
+ for (const rel of REL_CANDIDATES) {
61
+ const filePath = path.join(root, rel);
62
+ try {
63
+ if (!fs.existsSync(filePath))
64
+ continue;
65
+ const stat = fs.statSync(filePath);
66
+ if (!stat.isFile())
67
+ continue;
68
+ const key = `${filePath}:${Math.trunc(stat.mtimeMs)}:${stat.size}`;
69
+ if (key === cachedKey && cachedTransport) {
70
+ return { transport: cachedTransport, fromUser: cachedFromUser, source: filePath };
71
+ }
72
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
73
+ const cfg = extractSmtpConfig(parsed);
74
+ if (!cfg)
75
+ continue;
76
+ cachedTransport = nodemailer.createTransport(cfg.transportOpts);
77
+ cachedFromUser = cfg.fromUser;
78
+ cachedKey = key;
79
+ return { transport: cachedTransport, fromUser: cachedFromUser, source: filePath };
80
+ }
81
+ catch (e) {
82
+ if (e?.code !== 'ENOENT')
83
+ console.warn('[mail]', filePath, e?.message || e);
84
+ }
85
+ }
86
+ }
87
+ cachedKey = '';
88
+ cachedTransport = null;
89
+ cachedFromUser = '';
90
+ return null;
91
+ }
92
+
93
+ export const mailer = getMailer()?.transport || null;
@@ -50,14 +50,25 @@ function flush($server) {
50
50
  Promise.resolve(globalThis.WORK_AGENT_CORE?.()).then(c => c?.voice?.resetVoiceProbes?.()).catch(() => {});
51
51
  }
52
52
  for (const f of files) {
53
- const p = '/' + f.replace(/\\/g, '/');
53
+ const norm = f.replace(/\\/g, '/');
54
+ const p = '/' + norm;
55
+ if (!fs.existsSync(f)) {
56
+ const parentDir = path.posix.dirname(norm);
57
+ if (parentDir && parentDir !== '.' && fs.existsSync(parentDir)) {
58
+ Promise.resolve(globalThis.WORK?.get_item('/' + parentDir)).then(item => {
59
+ if (item && !Array.isArray(item))
60
+ item.reset?.();
61
+ }).catch(() => {});
62
+ }
63
+ continue;
64
+ }
54
65
  Promise.resolve(globalThis.WORK?.get_item(p)).then(item => {
55
66
  if (item && !Array.isArray(item))
56
67
  item.reset?.();
57
68
  }).catch(() => {});
58
69
  // Если изменился /X/$type/class.js — сбрасываем также сам узел класса /X
59
70
  const clsMatch = p.match(/^(\/.*?)\/\$[^/]+\/class\.js$/i);
60
- if (clsMatch?.[1]) {
71
+ if (clsMatch?.[1] && fs.existsSync(clsMatch[1].slice(1))) {
61
72
  Promise.resolve(globalThis.WORK?.get_item(clsMatch[1])).then(clsItem => {
62
73
  if (clsItem && !Array.isArray(clsItem))
63
74
  clsItem.reset?.();
@@ -259,7 +259,24 @@ export async function llmWithFallback(primaryPath, fallbackPaths = []) {
259
259
  };
260
260
  return adapter;
261
261
  }
262
- /** Первая модель с capability image (или ai/config.js imageModel). */
262
+ /** Первая доступная чат-модель в /MODELS (активная приоритетно), когда дефолтная модель отсутствует. */
263
+ async function findFirstAvailableChatModel() {
264
+ const root = await WORK.get_item('/MODELS').catch(() => null);
265
+ let firstAny = '';
266
+ for (const provider of (await root?.items) || []) {
267
+ for (const m of (await provider.items) || []) {
268
+ await m.init;
269
+ const caps = Array.isArray(m.capabilities) ? m.capabilities : String(m.capabilities || '').split(/[\s,]+/);
270
+ if (!caps.includes('chat') || typeof m.streamChat !== 'function')
271
+ continue;
272
+ if (!firstAny)
273
+ firstAny = m.path;
274
+ if ((m.aiActive ?? m.DATA?.aiActive) !== false)
275
+ return m.path;
276
+ }
277
+ }
278
+ return firstAny;
279
+ }
263
280
  async function findImageModel(config) {
264
281
  if (config.imageModel) {
265
282
  const m = await WORK.get_item(config.imageModel).catch(() => null);
@@ -530,7 +547,11 @@ export async function createEnv({ place, contextPath, session, host, tz, locatio
530
547
  return [stable, volatile].filter(Boolean).join('\n\n');
531
548
  },
532
549
  async defaultModel() {
533
- return config.model || DEFAULT_MODEL;
550
+ const candidate = config.model || DEFAULT_MODEL;
551
+ const item = await WORK.get_item(candidate).catch(() => null);
552
+ if (item)
553
+ return candidate;
554
+ return (await findFirstAvailableChatModel()) || candidate;
534
555
  },
535
556
  /** Память изменена инструментом memory — пересобрать блок на следующем ходе. */
536
557
  resetMemory() {