@iola_adm/iola-cli 0.2.44 → 0.2.46

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 CHANGED
@@ -207,6 +207,22 @@ Yandex tools уже доступны: профиль Yandex ID, расширен
207
207
 
208
208
  Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
209
209
 
210
+ Мой домофон:
211
+
212
+ ```bash
213
+ iola ufanet setup
214
+ iola ufanet intercoms
215
+ iola ufanet open ID
216
+ iola ufanet history
217
+ iola ufanet cameras
218
+ iola dom_ru
219
+ iola rostelecom
220
+ ```
221
+
222
+ Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков и камеры/RTSP. Дом.ру и Ростелеком добавлены как видимые направления `в разработке`.
223
+
224
+ Инструкция: [Мой домофон](https://github.com/adm-iola/iola-cli/wiki/Мой-домофон).
225
+
210
226
  Зарубежные API-ключи:
211
227
 
212
228
  - OpenAI Platform: регистрация `https://platform.openai.com/`, ключи `https://platform.openai.com/api-keys`;
@@ -236,6 +252,7 @@ iola version --check
236
252
  - [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key)
237
253
  - [Yandex Cloud Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Cloud-Connector)
238
254
  - [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector)
255
+ - [Мой домофон](https://github.com/adm-iola/iola-cli/wiki/Мой-домофон)
239
256
  - [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски)
240
257
  - [Скиллы для жителей](https://github.com/adm-iola/iola-cli/wiki/Скиллы-для-жителей)
241
258
  - [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
@@ -259,6 +276,7 @@ iola version --check
259
276
  - AI-профили для IOLA local, Ollama, YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
260
277
  - Yandex Connector: единая точка подключения пользовательских сервисов Яндекса с локальным хранением OAuth-токенов;
261
278
  - Yandex Cloud Connector: геокодер, YandexGPT и deeplink маршрута Яндекс Go;
279
+ - Мой домофон Уфанет: домофоны, история звонков, записи, камеры и открытие двери после подтверждения;
262
280
  - локальный tool-agent для модели IOLA с tools открытых данных, файлов, браузера и сервисов Яндекса;
263
281
  - ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
264
282
  - личные облачные диски: Яндекс Диск и Облако Mail.ru для сохранения отчетов, backup и документов;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.44",
3
+ "version": "0.2.46",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: ufanet-intercom
3
+ description: Мой домофон Уфанет: список домофонов, открытие двери после подтверждения, история звонков, записи звонков, камеры и RTSP.
4
+ ---
5
+
6
+ Используй этот skill, когда пользователь явно просит работать с домофоном Уфанет: открыть домофон, показать доступные домофоны, посмотреть историю звонков, получить запись звонка, показать камеры или RTSP.
7
+
8
+ Не смешивай Уфанет с Яндекс-сервисами, городскими открытыми слоями и локальными файлами. Это отдельный личный сервис пользователя.
9
+
10
+ Доступные tools:
11
+
12
+ - `ufanet_status` - проверить, подключен ли Уфанет.
13
+ - `ufanet_intercoms` - показать доступные домофоны пользователя.
14
+ - `ufanet_open_intercom` - открыть домофон по ID.
15
+ - `ufanet_call_history` - показать историю звонков домофона.
16
+ - `ufanet_call_links` - получить ссылку на запись/превью звонка по UUID.
17
+ - `ufanet_cameras` - показать доступные камеры и RTSP-ссылки.
18
+
19
+ Команды CLI:
20
+
21
+ - `iola ufanet setup` - подключить Уфанет.
22
+ - `iola ufanet status` - проверить подключение.
23
+ - `iola ufanet intercoms` - список домофонов.
24
+ - `iola ufanet open ID` - открыть домофон после подтверждения.
25
+ - `iola ufanet history` - история звонков.
26
+ - `iola ufanet links UUID` - ссылка на запись звонка.
27
+ - `iola ufanet cameras` - камеры.
28
+ - `iola ufanet delete` - удалить локальное подключение.
29
+
30
+ Безопасность:
31
+
32
+ - Для `ufanet_open_intercom` всегда нужен явный запрос пользователя и `confirm=true`.
33
+ - Если пользователь просит "открой домофон", но ID домофона неизвестен, сначала покажи список домофонов или попроси выбрать ID.
34
+ - Не открывай домофон по косвенному намерению вроде "кто там", "звонят", "посмотри".
35
+ - Не выводи договор, пароль, JWT-токен и другие секреты.
36
+ - Если Уфанет не подключен, скажи запустить `/master` и выбрать `Мой домофон Уфанет`, либо команду `iola ufanet setup`.
37
+
38
+ Дом.ру и Ростелеком:
39
+
40
+ - `/dom_ru` и `/rostelecom` пока являются заготовками "в разработке".
41
+ - Не обещай работу этих провайдеров, пока их API не реализован.
package/src/cli.js CHANGED
@@ -152,6 +152,7 @@ const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "
152
152
  const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
153
153
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
154
154
  const USER_SKILL_TOOLS = ["user_skill_create", "user_skill_update", "user_skill_enable", "user_skill_disable", "user_skill_delete", "user_skill_list", "user_skill_templates", "user_skill_validate", "user_skill_preview"];
155
+ const UFANET_TOOLS = ["ufanet_status", "ufanet_intercoms", "ufanet_open_intercom", "ufanet_call_history", "ufanet_call_links", "ufanet_cameras"];
155
156
  const YANDEX_TOOLS = [
156
157
  "yandex_identity_me",
157
158
  "yandex_disk_info",
@@ -251,7 +252,7 @@ const YANDEX_TOOLS = [
251
252
  "yandex_cloud_status",
252
253
  "yandex_go_deeplink",
253
254
  ];
254
- const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
255
+ const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...UFANET_TOOLS, ...USER_SKILL_TOOLS];
255
256
  const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
256
257
  const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
257
258
  const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
@@ -283,6 +284,13 @@ const TOOLSETS = {
283
284
  localTools: Object.fromEntries(YANDEX_TOOLS.map((tool) => [tool, true])),
284
285
  },
285
286
  },
287
+ ufanet: {
288
+ description: "Мой домофон Уфанет: домофоны, история звонков, камеры и открытие двери после подтверждения.",
289
+ permissions: {
290
+ externalApi: true,
291
+ localTools: Object.fromEntries(UFANET_TOOLS.map((tool) => [tool, true])),
292
+ },
293
+ },
286
294
  "local-files-read": {
287
295
  description: "Чтение файлов, дерево папок и поиск внутри workspace.",
288
296
  permissions: { readFiles: true, localTools: { files_tree: true, files_read: true, files_search: true } },
@@ -436,6 +444,12 @@ const DEFAULT_AI_CONFIG = {
436
444
  yandex_mail_search: true,
437
445
  yandex_mail_read: true,
438
446
  yandex_mail_send: false,
447
+ ufanet_status: true,
448
+ ufanet_intercoms: true,
449
+ ufanet_open_intercom: false,
450
+ ufanet_call_history: true,
451
+ ufanet_call_links: true,
452
+ ufanet_cameras: true,
439
453
  yandex_calendar_status: true,
440
454
  yandex_calendar_create_event: false,
441
455
  yandex_calendar_list: true,
@@ -475,6 +489,14 @@ const DEFAULT_AI_CONFIG = {
475
489
  skills: {
476
490
  enabled: ["education", "open-data", "geo", "personal-docs", "reports", "local-model", "local-files", "browser-agent", "yandex-services", "user-skills"],
477
491
  },
492
+ domophones: {
493
+ activeProvider: "",
494
+ providers: {
495
+ ufanet: { enabled: false },
496
+ domru: { enabled: false, status: "backlog" },
497
+ rostelecom: { enabled: false, status: "backlog" },
498
+ },
499
+ },
478
500
  cloud: {
479
501
  activeProvider: "",
480
502
  providers: {
@@ -577,6 +599,9 @@ const SLASH_COMMANDS = [
577
599
  { command: "/files status", description: "локальные файловые операции" },
578
600
  { command: "/cloud status", description: "облачные диски" },
579
601
  { command: "/yandex", description: "выбор сервисов Yandex Connector" },
602
+ { command: "/ufanet", description: "Мой домофон Уфанет" },
603
+ { command: "/dom_ru", description: "Мой домофон Дом.ру (в разработке)" },
604
+ { command: "/rostelecom", description: "Мой домофон Ростелеком (в разработке)" },
580
605
  { command: "/archive doctor", description: "архиватор" },
581
606
  { command: "/changes list", description: "подготовленные изменения" },
582
607
  { command: "/index status", description: "индекс документов" },
@@ -651,6 +676,10 @@ const COMMANDS = new Map([
651
676
  ["files", handleFiles],
652
677
  ["cloud", handleCloud],
653
678
  ["yandex", handleYandex],
679
+ ["ufanet", handleUfanet],
680
+ ["dom_ru", handleDomRu],
681
+ ["domru", handleDomRu],
682
+ ["rostelecom", handleRostelecom],
654
683
  ["archive", handleArchive],
655
684
  ["changes", handleChanges],
656
685
  ["import", handleImport],
@@ -801,6 +830,7 @@ async function showHelp() {
801
830
  iola browser status браузерный runtime
802
831
  iola cloud status облачные диски
803
832
  iola yandex status Yandex Connector
833
+ iola ufanet status Мой домофон Уфанет
804
834
  iola mcp status MCP-подключение
805
835
  iola doctor диагностика
806
836
  iola wiki документация
@@ -842,6 +872,9 @@ Usage:
842
872
  iola files status|mode|approvals|tree|read|search|write|patch
843
873
  iola cloud setup|status|ls|find|upload|download|share|save|backup
844
874
  iola yandex setup|menu|status|services|enable|disable|oauth-url|token
875
+ iola ufanet setup|status|intercoms|open|history|links|cameras|delete
876
+ iola dom_ru Мой домофон Дом.ру (в разработке)
877
+ iola rostelecom Мой домофон Ростелеком (в разработке)
845
878
  iola archive doctor|list|test|extract|create|index
846
879
  iola changes list|show|apply|discard
847
880
  iola import file|folder
@@ -3523,6 +3556,344 @@ async function handleYandex(args) {
3523
3556
  iola yandex backlog`);
3524
3557
  }
3525
3558
 
3559
+ async function handleDomRu() {
3560
+ console.log("Мой домофон Дом.ру: в разработке.");
3561
+ console.log("Пока доступна заготовка пункта в городских сервисах. API/авторизация будут добавлены после исследования провайдера.");
3562
+ }
3563
+
3564
+ async function handleRostelecom() {
3565
+ console.log("Мой домофон Ростелеком: в разработке.");
3566
+ console.log("Пока доступна заготовка пункта в городских сервисах. API/авторизация будут добавлены после исследования провайдера.");
3567
+ }
3568
+
3569
+ async function handleUfanet(args = []) {
3570
+ const [action = process.stdin.isTTY ? "menu" : "status", target, ...rest] = args;
3571
+ const options = parseOptions(rest);
3572
+
3573
+ if (action === "menu" || action === "choose") {
3574
+ await printUfanetMenu();
3575
+ return;
3576
+ }
3577
+
3578
+ if (action === "setup" || action === "connect" || action === "onboard") {
3579
+ await setupUfanetConnector();
3580
+ return;
3581
+ }
3582
+
3583
+ if (action === "status" || action === "doctor" || action === "check") {
3584
+ await printUfanetStatus({ check: action !== "status" || options.check });
3585
+ return;
3586
+ }
3587
+
3588
+ if (action === "intercoms" || action === "list" || action === "ls") {
3589
+ const rows = await ufanetGetIntercoms();
3590
+ printTable(rows, [["id", "ID"], ["name", "Название"], ["address", "Адрес"], ["role", "Роль"], ["blocked", "Блок"]]);
3591
+ return;
3592
+ }
3593
+
3594
+ if (action === "open") {
3595
+ const intercomId = target || options.id || options.intercom;
3596
+ if (!intercomId) throw new Error("Укажите ID домофона. Пример: iola ufanet open 123");
3597
+ const ok = options.yes || options.confirm || await confirm(`Открыть домофон Уфанет #${intercomId}? [y/N] `);
3598
+ if (!ok) {
3599
+ console.log("Открытие отменено.");
3600
+ return;
3601
+ }
3602
+ printKeyValue(await ufanetOpenIntercom(intercomId, { confirm: true }));
3603
+ return;
3604
+ }
3605
+
3606
+ if (action === "history" || action === "calls") {
3607
+ const rows = await ufanetGetCallHistory({ page: target || options.page || 1, pageSize: options.limit || options["page-size"] || 10 });
3608
+ printTable(rows.results || [], [["uuid", "UUID"], ["calledAt", "Когда"], ["address", "Адрес"], ["porch", "Подъезд"], ["flat", "Кв"]]);
3609
+ if (rows.count !== undefined) console.log(`Всего: ${rows.count}`);
3610
+ return;
3611
+ }
3612
+
3613
+ if (action === "links" || action === "record" || action === "recording") {
3614
+ const uuid = target || options.uuid;
3615
+ if (!uuid) throw new Error("Укажите UUID звонка. Пример: iola ufanet links UUID");
3616
+ printKeyValue(await ufanetGetCallLinks(uuid));
3617
+ return;
3618
+ }
3619
+
3620
+ if (action === "cameras" || action === "camera") {
3621
+ const rows = await ufanetGetCameras();
3622
+ printTable(rows, [["number", "Номер"], ["title", "Название"], ["address", "Адрес"], ["type", "Тип"], ["rtspUrl", "RTSP"]]);
3623
+ return;
3624
+ }
3625
+
3626
+ if (action === "delete" || action === "disconnect" || action === "remove") {
3627
+ const ok = !process.stdin.isTTY || await askYesNo("Удалить локальные данные подключения Уфанет? [y/N] ", false);
3628
+ if (!ok) {
3629
+ console.log("Удаление отменено.");
3630
+ return;
3631
+ }
3632
+ await deleteUfanetConnector();
3633
+ return;
3634
+ }
3635
+
3636
+ throw new Error(`Команды ufanet:
3637
+ iola ufanet setup
3638
+ iola ufanet status|doctor
3639
+ iola ufanet intercoms
3640
+ iola ufanet open ID
3641
+ iola ufanet history [--limit 10]
3642
+ iola ufanet links UUID
3643
+ iola ufanet cameras
3644
+ iola ufanet delete`);
3645
+ }
3646
+
3647
+ async function printUfanetMenu() {
3648
+ const status = await getUfanetStatus();
3649
+ console.log("Мой домофон");
3650
+ printTable([
3651
+ { id: "ufanet", provider: "Уфанет", status: status.configured ? "готово" : "не настроено", command: "iola ufanet setup" },
3652
+ { id: "domru", provider: "Дом.ру", status: "в разработке", command: "iola dom_ru" },
3653
+ { id: "rostelecom", provider: "Ростелеком", status: "в разработке", command: "iola rostelecom" },
3654
+ ], [["id", "ID"], ["provider", "Провайдер"], ["status", "Статус"], ["command", "Команда"]]);
3655
+ }
3656
+
3657
+ async function setupUfanetConnector() {
3658
+ console.log("Мой домофон Уфанет.");
3659
+ console.log("Нужны номер договора и пароль от сервиса Уфанет. Они сохраняются только локально в ~/.iola/secrets.json.");
3660
+ if (!process.stdin.isTTY) {
3661
+ console.log("Интерактивный ввод недоступен. Используйте env UFANET_CONTRACT и UFANET_PASSWORD или запустите iola ufanet setup в терминале.");
3662
+ return;
3663
+ }
3664
+ const secrets = await loadSecrets();
3665
+ const currentContract = secrets.ufanet?.contract || "";
3666
+ const contract = (await askText(`Номер договора${currentContract ? " [Enter - оставить]" : ""}: `)).trim() || currentContract;
3667
+ const password = (await askText(`Пароль Уфанет${secrets.ufanet?.password ? " [Enter - оставить]" : ""}: `)).trim() || secrets.ufanet?.password || "";
3668
+ if (!contract || !password) throw new Error("Для Уфанет нужны номер договора и пароль.");
3669
+ await saveUfanetConnectorSecrets({ contract, password });
3670
+ await enableUfanetConnector();
3671
+ console.log(`Уфанет сохранен локально: ${SECRETS_FILE}`);
3672
+ await printUfanetStatus({ check: true });
3673
+ }
3674
+
3675
+ async function saveUfanetConnectorSecrets({ contract, password }) {
3676
+ const secrets = await loadSecrets();
3677
+ secrets.ufanet = {
3678
+ ...(secrets.ufanet || {}),
3679
+ contract,
3680
+ password,
3681
+ updatedAt: new Date().toISOString(),
3682
+ };
3683
+ await saveSecrets(secrets);
3684
+ }
3685
+
3686
+ async function enableUfanetConnector() {
3687
+ const config = await loadConfig();
3688
+ await saveConfig({
3689
+ domophones: {
3690
+ ...(config.domophones || {}),
3691
+ activeProvider: "ufanet",
3692
+ providers: {
3693
+ ...(config.domophones?.providers || {}),
3694
+ ufanet: { ...(config.domophones?.providers?.ufanet || {}), enabled: true, updatedAt: new Date().toISOString() },
3695
+ domru: { ...(config.domophones?.providers?.domru || {}), enabled: false, status: "backlog" },
3696
+ rostelecom: { ...(config.domophones?.providers?.rostelecom || {}), enabled: false, status: "backlog" },
3697
+ },
3698
+ },
3699
+ toolsets: { ...(config.toolsets || {}), enabled: [...new Set([...(config.toolsets?.enabled || []), "ufanet"])] },
3700
+ skills: { ...(config.skills || {}), enabled: [...new Set([...(config.skills?.enabled || []), "ufanet-intercom"])] },
3701
+ });
3702
+ }
3703
+
3704
+ async function deleteUfanetConnector() {
3705
+ const secrets = await loadSecrets();
3706
+ delete secrets.ufanet;
3707
+ await saveSecrets(secrets);
3708
+ const config = await loadConfig();
3709
+ const enabledToolsets = (config.toolsets?.enabled || []).filter((item) => item !== "ufanet");
3710
+ const enabledSkills = (config.skills?.enabled || []).filter((item) => item !== "ufanet-intercom");
3711
+ await saveConfig({
3712
+ domophones: {
3713
+ ...(config.domophones || {}),
3714
+ activeProvider: config.domophones?.activeProvider === "ufanet" ? "" : config.domophones?.activeProvider || "",
3715
+ providers: {
3716
+ ...(config.domophones?.providers || {}),
3717
+ ufanet: { ...(config.domophones?.providers?.ufanet || {}), enabled: false },
3718
+ },
3719
+ },
3720
+ toolsets: { ...(config.toolsets || {}), enabled: enabledToolsets },
3721
+ skills: { ...(config.skills || {}), enabled: enabledSkills },
3722
+ });
3723
+ console.log("Подключение Уфанет удалено локально.");
3724
+ }
3725
+
3726
+ async function printUfanetStatus(options = {}) {
3727
+ const status = await getUfanetStatus();
3728
+ printKeyValue({
3729
+ configured: status.configured ? "yes" : "no",
3730
+ enabled: status.enabled ? "yes" : "no",
3731
+ contract: status.contract || "-",
3732
+ source: status.source || "-",
3733
+ });
3734
+ if (options.check) {
3735
+ if (!status.configured) {
3736
+ console.log("Уфанет: не настроен. Запустите: iola ufanet setup");
3737
+ return;
3738
+ }
3739
+ try {
3740
+ const rows = await ufanetGetIntercoms();
3741
+ console.log(`Уфанет: ok, домофонов: ${rows.length}`);
3742
+ } catch (error) {
3743
+ console.log(`Уфанет: ошибка проверки: ${error instanceof Error ? error.message : String(error)}`);
3744
+ }
3745
+ }
3746
+ }
3747
+
3748
+ async function getUfanetStatus() {
3749
+ const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3750
+ const contract = process.env.UFANET_CONTRACT || secrets.ufanet?.contract || "";
3751
+ const password = process.env.UFANET_PASSWORD || secrets.ufanet?.password || "";
3752
+ return {
3753
+ configured: Boolean(contract && password),
3754
+ enabled: Boolean(config.domophones?.providers?.ufanet?.enabled || (config.toolsets?.enabled || []).includes("ufanet")),
3755
+ contract: contract ? maskSecret(contract, 2) : "",
3756
+ source: contract && process.env.UFANET_CONTRACT ? "env" : contract ? "local" : "",
3757
+ };
3758
+ }
3759
+
3760
+ async function executeUfanetTool(tool, args = {}) {
3761
+ if (tool === "ufanet_status") return getUfanetStatus();
3762
+ if (tool === "ufanet_intercoms") return ufanetGetIntercoms();
3763
+ if (tool === "ufanet_open_intercom") return ufanetOpenIntercom(args.id || args.intercomId || args.intercom_id, args);
3764
+ if (tool === "ufanet_call_history") return ufanetGetCallHistory({ page: args.page || 1, pageSize: args.pageSize || args.page_size || args.limit || 10 });
3765
+ if (tool === "ufanet_call_links") return ufanetGetCallLinks(args.uuid || args.id);
3766
+ if (tool === "ufanet_cameras") return ufanetGetCameras();
3767
+ throw new Error(`Ufanet tool неизвестен: ${tool}`);
3768
+ }
3769
+
3770
+ async function ufanetCredentials() {
3771
+ const secrets = await loadSecrets();
3772
+ const contract = process.env.UFANET_CONTRACT || secrets.ufanet?.contract || "";
3773
+ const password = process.env.UFANET_PASSWORD || secrets.ufanet?.password || "";
3774
+ if (!contract || !password) throw new Error("Уфанет не подключен. Запустите: iola ufanet setup");
3775
+ return { contract, password, token: secrets.ufanet?.token || "", tokenUpdatedAt: secrets.ufanet?.tokenUpdatedAt || "" };
3776
+ }
3777
+
3778
+ async function ufanetEnsureToken(options = {}) {
3779
+ const credentials = await ufanetCredentials();
3780
+ if (credentials.token && !options.force) return credentials.token;
3781
+ const response = await fetch("https://dom.ufanet.ru/api/v1/auth/auth_by_contract/", {
3782
+ method: "POST",
3783
+ headers: { accept: "application/json", "content-type": "application/json" },
3784
+ body: JSON.stringify({ contract: credentials.contract, password: credentials.password }),
3785
+ signal: AbortSignal.timeout(30000),
3786
+ });
3787
+ const payload = await parseJsonResponse(response, "Уфанет авторизация");
3788
+ const token = payload?.token?.refresh || payload?.token?.access || payload?.refresh || payload?.access || "";
3789
+ if (!token) throw new Error("Уфанет не вернул JWT token.");
3790
+ if (!process.env.UFANET_CONTRACT && !process.env.UFANET_PASSWORD) {
3791
+ const secrets = await loadSecrets();
3792
+ secrets.ufanet = { ...(secrets.ufanet || {}), token, tokenUpdatedAt: new Date().toISOString() };
3793
+ await saveSecrets(secrets);
3794
+ }
3795
+ return token;
3796
+ }
3797
+
3798
+ async function ufanetRequest(method, apiPath, options = {}) {
3799
+ let token = await ufanetEnsureToken();
3800
+ let response = await fetch(new URL(apiPath, "https://dom.ufanet.ru/"), {
3801
+ method,
3802
+ headers: { accept: "application/json", "content-type": "application/json", Authorization: `JWT ${token}` },
3803
+ body: options.body ? JSON.stringify(options.body) : undefined,
3804
+ signal: AbortSignal.timeout(Number(options.timeout || 30000)),
3805
+ });
3806
+ if (response.status === 401) {
3807
+ token = await ufanetEnsureToken({ force: true });
3808
+ response = await fetch(new URL(apiPath, "https://dom.ufanet.ru/"), {
3809
+ method,
3810
+ headers: { accept: "application/json", "content-type": "application/json", Authorization: `JWT ${token}` },
3811
+ body: options.body ? JSON.stringify(options.body) : undefined,
3812
+ signal: AbortSignal.timeout(Number(options.timeout || 30000)),
3813
+ });
3814
+ }
3815
+ return parseJsonResponse(response, `Уфанет ${apiPath}`);
3816
+ }
3817
+
3818
+ async function parseJsonResponse(response, label) {
3819
+ const text = await response.text().catch(() => "");
3820
+ let payload = null;
3821
+ if (text) {
3822
+ try {
3823
+ payload = JSON.parse(text);
3824
+ } catch {
3825
+ payload = text;
3826
+ }
3827
+ }
3828
+ if (!response.ok) {
3829
+ const message = typeof payload === "string" ? payload.slice(0, 300) : JSON.stringify(payload).slice(0, 300);
3830
+ throw new Error(`${label}: ${response.status} ${response.statusText}${message ? ` ${message}` : ""}`);
3831
+ }
3832
+ return payload;
3833
+ }
3834
+
3835
+ async function ufanetGetIntercoms() {
3836
+ const payload = await ufanetRequest("GET", "api/v0/skud/shared/");
3837
+ return normalizeItems(payload).map((item) => ({
3838
+ id: item.id,
3839
+ name: item.custom_name || item.string_view || `Домофон ${item.id}`,
3840
+ address: item.string_view || "",
3841
+ role: item.role?.name || "",
3842
+ camera: item.camera || item.cctv_number || "",
3843
+ blocked: item.is_blocked ? "yes" : "no",
3844
+ disableButton: Boolean(item.disable_button),
3845
+ inactivityReason: item.inactivity_reason || "",
3846
+ raw: item,
3847
+ }));
3848
+ }
3849
+
3850
+ async function ufanetOpenIntercom(intercomId, options = {}) {
3851
+ if (!options.confirm) throw new Error("Для открытия домофона нужен аргумент confirm=true.");
3852
+ if (!intercomId) throw new Error("ID домофона обязателен.");
3853
+ const payload = await ufanetRequest("GET", `api/v0/skud/shared/${encodeURIComponent(intercomId)}/open/`, { timeout: 30000 });
3854
+ return { provider: "ufanet", status: payload?.result ? "opened" : "not-opened", id: Number(intercomId), result: Boolean(payload?.result) };
3855
+ }
3856
+
3857
+ async function ufanetGetCallHistory(options = {}) {
3858
+ const page = Math.max(1, Number(options.page || 1));
3859
+ const pageSize = Math.max(1, Math.min(100, Number(options.pageSize || 10)));
3860
+ const url = `api/v1/skuds/call-history/?page=${encodeURIComponent(page)}&page_size=${encodeURIComponent(pageSize)}`;
3861
+ const payload = await ufanetRequest("GET", url, { timeout: 30000 });
3862
+ return {
3863
+ count: payload?.count || 0,
3864
+ next: payload?.next || "",
3865
+ previous: payload?.previous || "",
3866
+ results: normalizeItems(payload?.results || []).map((item) => ({
3867
+ uuid: item.uuid,
3868
+ calledAt: item.called_at || item.calledAt || "",
3869
+ address: item.address || "",
3870
+ porch: item.porch || "",
3871
+ flat: item.flat || "",
3872
+ cameraNumber: item.camera_number || "",
3873
+ timezone: item.timezone || "",
3874
+ })),
3875
+ };
3876
+ }
3877
+
3878
+ async function ufanetGetCallLinks(uuid) {
3879
+ if (!uuid) throw new Error("UUID звонка обязателен.");
3880
+ const payload = await ufanetRequest("POST", "api/v1/cctv/history/", { body: { uuid: String(uuid) }, timeout: 30000 });
3881
+ return { provider: "ufanet", uuid: String(uuid), url: payload?.url || "", preview: payload?.preview || "" };
3882
+ }
3883
+
3884
+ async function ufanetGetCameras() {
3885
+ const payload = await ufanetRequest("GET", "api/v1/cctv", { timeout: 30000 });
3886
+ return normalizeItems(payload).map((item) => ({
3887
+ number: item.number || "",
3888
+ title: item.title || "",
3889
+ address: item.address || "",
3890
+ latitude: item.latitude,
3891
+ longitude: item.longitude,
3892
+ type: item.type || "",
3893
+ rtspUrl: item.servers?.domain && item.number && item.token_l ? `rtsp://${item.servers.domain}/${item.number}?token=${item.token_l}` : "",
3894
+ }));
3895
+ }
3896
+
3526
3897
  function printYandexServices(options = {}) {
3527
3898
  const rows = Object.entries(YANDEX_CONNECTOR_SERVICES)
3528
3899
  .filter(([, service]) => !options.status || service.status === options.status)
@@ -14545,7 +14916,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
14545
14916
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
14546
14917
  "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
14547
14918
  "Yandex tools: yandex_identity_me {}, yandex_disk_info {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_stat {path}, yandex_disk_exists {path}, yandex_disk_read_text {path}, yandex_disk_save_text {path,text}, yandex_disk_upload {localPath,remotePath}, yandex_disk_download {remotePath,outputPath}, yandex_disk_move {from,to,confirm}, yandex_disk_copy {from,to,confirm}, yandex_disk_rename {path,name,confirm}, yandex_disk_share {path,confirm}, yandex_disk_share_qr {path,confirm}, yandex_disk_share_email {path,to,contact,subject,text,confirm}, yandex_disk_package_share_email {sourcePath,targetFolder,to,contact,mode,confirm}, yandex_disk_unshare {path}, yandex_disk_delete {path,confirm}, yandex_disk_trash_list {}, yandex_disk_restore {path,confirm}, yandex_disk_empty_trash {confirm}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_send {to,subject,text,confirm}, yandex_mail_reply {uid,text,confirm}, yandex_mail_forward {uid,to,confirm}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_mail_meeting_pack {uid,start,end,send,confirm}, yandex_calendar_calendars {}, yandex_calendar_list {start,end}, yandex_calendar_search {query,start,end}, yandex_calendar_get {query}, yandex_calendar_create_event {title,start,end,location,attendees,reminders,confirm}, yandex_calendar_update {query,title,start,end,location,description,reminders,confirm}, yandex_calendar_move {query,start,end,confirm}, yandex_calendar_delete {query,confirm}, yandex_docs_list {path}, yandex_docs_find {query}, yandex_docs_create_text {title,text,format,confirm}, yandex_docs_read {path|query}, yandex_docs_share {path|query,confirm}, yandex_docs_rename {path|query,name,confirm}, yandex_docs_delete {path|query,confirm}, yandex_contacts_list {limit}, yandex_contacts_search {query}, yandex_contacts_get {query}, yandex_contacts_create {name,email,phone,address,note,confirm}, yandex_contacts_update {query,email,phone,address,note,birthday,org,title,confirm}, yandex_contacts_delete {query,confirm}, yandex_contacts_export_csv {}, yandex_contacts_find_incomplete {}, yandex_contacts_find_duplicates {}, yandex_contacts_backup_to_disk {format,confirm}, yandex_contact_send_mail {contact,subject,text,confirm}, yandex_contact_send_disk_link_qr {contact,path,confirm}, yandex_contact_create_disk_folder {contact,confirm}, yandex_contact_create_calendar_event {contact,start,end,title,confirm}, yandex_contact_create_telemost_event {contact,start,end,title,confirm}, yandex_contact_full_pack {contact,start,end,send,confirm}, yandex_cloud_status {}, yandex_go_deeplink {from,to,tariff}, yandex_daily_digest {save,email}, yandex_calendar_reminders_tick {}, yandex_disk_maintenance_tick {}.",
14548
- "Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_mail_meeting_pack, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_contact_full_pack, yandex_calendar_create_event, yandex_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event.",
14919
+ "Ufanet tools: ufanet_status {}, ufanet_intercoms {}, ufanet_open_intercom {id,confirm}, ufanet_call_history {page,limit}, ufanet_call_links {uuid}, ufanet_cameras {}.",
14920
+ "Опасные Yandex/Ufanet tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_mail_meeting_pack, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_contact_full_pack, yandex_calendar_create_event, yandex_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event, ufanet_open_intercom.",
14549
14921
  "User skill tools: user_skill_create {name,description,instructions,tools,template,enable,confirm}, user_skill_update {name,instructions,tools,confirm}, user_skill_templates {}, user_skill_validate {name}, user_skill_preview {name,template,instructions}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай или меняй skill только по явной просьбе пользователя и с confirm=true.",
14550
14922
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
14551
14923
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
@@ -14602,6 +14974,16 @@ function parseJsonObject(text) {
14602
14974
  return JSON.parse(match[0]);
14603
14975
  }
14604
14976
 
14977
+ function extractUfanetIntercomId(text) {
14978
+ return String(text || "").match(/(?:домофон|id|#|№)\s*(\d{1,10})/iu)?.[1]
14979
+ || String(text || "").match(/\b(\d{2,10})\b/u)?.[1]
14980
+ || "";
14981
+ }
14982
+
14983
+ function extractUuid(text) {
14984
+ return String(text || "").match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/iu)?.[0] || "";
14985
+ }
14986
+
14605
14987
  function inferToolPlan(question, options = {}) {
14606
14988
  const normalized = question.toLocaleLowerCase("ru-RU");
14607
14989
  if (isCurrentDateTimeQuestion(normalized)) {
@@ -14635,6 +15017,26 @@ function inferToolPlan(question, options = {}) {
14635
15017
  if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
14636
15018
  return { steps: [{ tool: "yandex_identity_me", args: {} }] };
14637
15019
  }
15020
+ if (/(уфанет|домофон|домофонн|подъезд|звонк|камера|rtsp)/iu.test(normalized)) {
15021
+ if (/(дом\.?ру|dom\.?ru)/iu.test(normalized)) return { directAnswer: "Мой домофон Дом.ру пока в разработке. Сейчас реализован Уфанет: /ufanet." };
15022
+ if (/(ростелеком|rostelecom)/iu.test(normalized)) return { directAnswer: "Мой домофон Ростелеком пока в разработке. Сейчас реализован Уфанет: /ufanet." };
15023
+ if (/(открой|открыть|пусти|впусти|двер)/iu.test(normalized)) {
15024
+ const id = extractUfanetIntercomId(question);
15025
+ if (!id) return { directAnswer: "Для открытия домофона нужен ID. Посмотрите доступные домофоны командой /ufanet intercoms, затем: /ufanet open ID." };
15026
+ return { steps: [{ tool: "ufanet_open_intercom", args: { id, confirm: true } }] };
15027
+ }
15028
+ if (/(истори|звонк|кто\s+звонил|последн)/iu.test(normalized) && !/(ссылк|запис|видео)/iu.test(normalized)) {
15029
+ return { steps: [{ tool: "ufanet_call_history", args: { limit: 10 } }] };
15030
+ }
15031
+ if (/(ссылк|запис|видео|preview|превью)/iu.test(normalized)) {
15032
+ const uuid = extractUuid(question);
15033
+ if (!uuid) return { directAnswer: "Для ссылки на запись нужен UUID звонка. Сначала посмотрите историю: /ufanet history." };
15034
+ return { steps: [{ tool: "ufanet_call_links", args: { uuid } }] };
15035
+ }
15036
+ if (/(камер|rtsp|видео)/iu.test(normalized)) return { steps: [{ tool: "ufanet_cameras", args: {} }] };
15037
+ if (/(статус|подключ|аккаунт|договор)/iu.test(normalized)) return { steps: [{ tool: "ufanet_status", args: {} }] };
15038
+ return { steps: [{ tool: "ufanet_intercoms", args: {} }] };
15039
+ }
14638
15040
  if (/(яндекс|диск|облак)/iu.test(normalized)) {
14639
15041
  const diskPath = extractCloudPath(question) || CLOUD_DEFAULT_REMOTE_DIR;
14640
15042
  if (/(мест[оа]|сколько.*занято|сколько.*свобод|статус|инфо)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_info", args: {} }] };
@@ -15057,7 +15459,7 @@ function formatToolExecutionError(error, plan) {
15057
15459
  }
15058
15460
 
15059
15461
  function availableToolNames(options = {}) {
15060
- const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS]);
15462
+ const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS, ...UFANET_TOOLS, ...USER_SKILL_TOOLS]);
15061
15463
  if (options.files) {
15062
15464
  for (const tool of FILE_TOOLS) names.add(tool);
15063
15465
  }
@@ -15126,6 +15528,11 @@ async function executeToolPlan(plan, options = {}) {
15126
15528
  const result = await executeYandexTool(step.tool, step.args || {});
15127
15529
  current = Array.isArray(result) ? result : [result];
15128
15530
  outputs.push({ tool: step.tool, rows: current.length });
15531
+ } else if (UFANET_TOOLS.includes(step.tool)) {
15532
+ await assertPermission("externalApi");
15533
+ const result = await executeUfanetTool(step.tool, step.args || {});
15534
+ current = Array.isArray(result) ? result : [result];
15535
+ outputs.push({ tool: step.tool, rows: current.length });
15129
15536
  } else if (USER_SKILL_TOOLS.includes(step.tool)) {
15130
15537
  const result = await executeUserSkillTool(step.tool, step.args || {});
15131
15538
  current = Array.isArray(result) ? result : [result];
@@ -15281,6 +15688,12 @@ function formatToolResult(result, options) {
15281
15688
  return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
15282
15689
  }
15283
15690
  if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
15691
+ if (row.provider === "ufanet" && (row.status === "opened" || row.status === "not-opened")) return `Уфанет: домофон #${row.id} ${row.status === "opened" ? "открыт" : "не открылся"}.`;
15692
+ if (row.provider === "ufanet" && row.uuid && (row.url || row.preview)) return `Уфанет: запись звонка ${row.uuid}\nСсылка: ${row.url || "-"}\nПревью: ${row.preview || "-"}`;
15693
+ if (row.rtspUrl) return `Камера Уфанет ${row.title || row.number}: ${row.address || "-"}\nRTSP: ${row.rtspUrl}`;
15694
+ if (row.calledAt && row.uuid) return `Звонок Уфанет: ${row.calledAt}, ${row.address || "-"}, подъезд ${row.porch || "-"}, UUID ${row.uuid}`;
15695
+ if (row.id && (row.address || row.role || row.blocked !== undefined)) return `Домофон Уфанет #${row.id}: ${row.name || row.address || "-"}${row.blocked === "yes" ? " (заблокирован)" : ""}`;
15696
+ if (row.configured !== undefined && row.enabled !== undefined && row.contract !== undefined) return `Уфанет: ${row.configured ? "настроен" : "не настроен"}, ${row.enabled ? "включен" : "выключен"}${row.contract ? `, договор ${row.contract}` : ""}.`;
15284
15697
  if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
15285
15698
  return `${row.status === "calendar-event-created" ? "Событие" : "Телемост"} создан: ${row.title || row.uid}${row.start ? `, ${row.start}` : ""}${row.telemost?.joinUrl ? `\nСсылка: ${row.telemost.joinUrl}` : row.status === "telemost-calendar-fallback-created" ? "\nПрямая ссылка Телемоста через API недоступна, создано событие календаря." : ""}`;
15286
15699
  }
@@ -16668,6 +17081,10 @@ async function onboard(args = []) {
16668
17081
  if (components.includes("workspace")) await handleWorkspace(["init"]);
16669
17082
  if (components.includes("policy")) await handlePolicy(["use", "analyst"]);
16670
17083
  if (components.includes("archive")) await ensureArchiveTool({ install: true });
17084
+ if (components.includes("city-data")) await checkHealth([]);
17085
+ if (components.includes("ufanet")) await setupUfanetConnector();
17086
+ if (components.includes("domru")) await handleDomRu();
17087
+ if (components.includes("rostelecom")) await handleRostelecom();
16671
17088
  if (components.includes("iola")) {
16672
17089
  await setupIolaLocal(["--yes"]);
16673
17090
  }
@@ -16754,15 +17171,19 @@ async function chooseOnboardComponents(status = null) {
16754
17171
  3: "archive",
16755
17172
  4: "index",
16756
17173
  5: "browser",
16757
- 6: "iola",
16758
- 7: "ollama",
16759
- 8: "gigachat",
16760
- 9: "yandex",
16761
- 10: "yandex-cloud",
16762
- 11: "openai",
16763
- 12: "openrouter",
16764
- 13: "codex",
16765
- 14: "codex-mcp",
17174
+ 6: "city-data",
17175
+ 7: "codex-mcp",
17176
+ 8: "ufanet",
17177
+ 9: "domru",
17178
+ 10: "rostelecom",
17179
+ 11: "iola",
17180
+ 12: "ollama",
17181
+ 13: "gigachat",
17182
+ 14: "yandex",
17183
+ 15: "yandex-cloud",
17184
+ 16: "openai",
17185
+ 17: "openrouter",
17186
+ 18: "codex",
16766
17187
  };
16767
17188
  return [...selected].map((item) => map[item] || item).filter(Boolean);
16768
17189
  } finally {
@@ -16775,7 +17196,7 @@ function isOnboardExitAnswer(answer) {
16775
17196
  }
16776
17197
 
16777
17198
  async function getOnboardComponentStatus() {
16778
- const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey, secrets] = await Promise.all([
17199
+ const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey, secrets, cityDataHealth] = await Promise.all([
16779
17200
  loadConfig(),
16780
17201
  getAiReadiness(),
16781
17202
  getBrowserStatus(),
@@ -16784,6 +17205,7 @@ async function getOnboardComponentStatus() {
16784
17205
  getOllamaVersion(),
16785
17206
  getYandexGeocoderKey(),
16786
17207
  loadSecrets(),
17208
+ getMcpBaseUrl().then((baseUrl) => probeEndpoint(`${baseUrl}/mcp-health`)),
16787
17209
  ]);
16788
17210
  const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
16789
17211
  const policyReady = (config.toolsets?.enabled || []).includes("analyst");
@@ -16798,6 +17220,10 @@ async function getOnboardComponentStatus() {
16798
17220
  openrouter: Boolean(readiness.openrouter),
16799
17221
  codex: Boolean(codexVersion !== "не найден" && readiness.codex),
16800
17222
  "codex-mcp": false,
17223
+ "city-data": cityDataHealth === "доступен",
17224
+ ufanet: Boolean((process.env.UFANET_CONTRACT && process.env.UFANET_PASSWORD) || (secrets.ufanet?.contract && secrets.ufanet?.password)),
17225
+ domru: false,
17226
+ rostelecom: false,
16801
17227
  archive: Boolean(archive),
16802
17228
  index: false,
16803
17229
  browser: browser.installed === "yes",
@@ -16817,39 +17243,48 @@ function onboardComponentGroups(status) {
16817
17243
  ["5", "browser", "Браузерный модуль", "Playwright/Chromium"],
16818
17244
  ],
16819
17245
  },
17246
+ {
17247
+ title: "Городские сервисы",
17248
+ rows: [
17249
+ ["6", "city-data", "Открытые данные Йошкар-Олы", "API/MCP gateway доступен"],
17250
+ ["7", "codex-mcp", "Подключить городские данные к Codex", "MCP для Codex"],
17251
+ ["8", "ufanet", "Мой домофон Уфанет", "договор и пароль хранятся локально"],
17252
+ ["9", "domru", "Мой домофон Дом.ру", "в разработке"],
17253
+ ["10", "rostelecom", "Мой домофон Ростелеком", "в разработке"],
17254
+ ],
17255
+ },
16820
17256
  {
16821
17257
  title: "Локальный AI",
16822
17258
  rows: [
16823
- ["6", "iola", "IOLA локальная модель", "локальная модель найдена"],
16824
- ["7", "ollama", "Ollama", "опциональный локальный runtime"],
17259
+ ["11", "iola", "IOLA локальная модель", "локальная модель найдена"],
17260
+ ["12", "ollama", "Ollama", "опциональный локальный runtime"],
16825
17261
  ],
16826
17262
  },
16827
17263
  {
16828
17264
  title: "Российские AI и сервисы",
16829
17265
  rows: [
16830
- ["8", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
16831
- ["9", "yandex", "Yandex Connector", "Диск, Почта, Календарь, Контакты"],
16832
- ["10", "yandex-cloud", "Yandex Cloud Connector", "геокодинг и YandexGPT"],
17266
+ ["13", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
17267
+ ["14", "yandex", "Yandex Connector", "Диск, Почта, Календарь, Контакты"],
17268
+ ["15", "yandex-cloud", "Yandex Cloud Connector", "геокодинг и YandexGPT"],
16833
17269
  ],
16834
17270
  },
16835
17271
  {
16836
17272
  title: "Зарубежные AI",
16837
17273
  rows: [
16838
- ["11", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
16839
- ["12", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
17274
+ ["16", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
17275
+ ["17", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
16840
17276
  ],
16841
17277
  },
16842
17278
  {
16843
17279
  title: "Codex",
16844
17280
  rows: [
16845
- ["13", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
16846
- ["14", "codex-mcp", "Подключение IOLA к Codex", "MCP для Codex"],
17281
+ ["18", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
16847
17282
  ],
16848
17283
  },
16849
17284
  ];
16850
17285
  return groups.map((group) => ({
16851
17286
  ...group,
16852
- rows: group.rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" })),
17287
+ rows: group.rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: key === "domru" || key === "rostelecom" ? "в разработке" : status[key] ? "готово" : "не настроено" })),
16853
17288
  }));
16854
17289
  }
16855
17290
 
@@ -16862,12 +17297,12 @@ function defaultOnboardSelection(status) {
16862
17297
  if (!status.workspace) defaults.push("1");
16863
17298
  if (!status.policy) defaults.push("2");
16864
17299
  if (!status.archive) defaults.push("3");
16865
- if (!status.iola) defaults.push("6");
17300
+ if (!status.iola) defaults.push("11");
16866
17301
  return defaults.length ? defaults : ["1", "2"];
16867
17302
  }
16868
17303
 
16869
17304
  function defaultOnboardComponents(status) {
16870
- const map = { 1: "workspace", 2: "policy", 3: "archive", 4: "index", 5: "browser", 6: "iola", 7: "ollama", 8: "gigachat", 9: "yandex", 10: "yandex-cloud", 11: "openai", 12: "openrouter", 13: "codex", 14: "codex-mcp" };
17305
+ const map = { 1: "workspace", 2: "policy", 3: "archive", 4: "index", 5: "browser", 6: "city-data", 7: "codex-mcp", 8: "ufanet", 9: "domru", 10: "rostelecom", 11: "iola", 12: "ollama", 13: "gigachat", 14: "yandex", 15: "yandex-cloud", 16: "openai", 17: "openrouter", 18: "codex" };
16871
17306
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
16872
17307
  }
16873
17308
 
@@ -16881,7 +17316,7 @@ function parseOptions(args) {
16881
17316
  } else if (arg === "--check" || arg === "--upgrade-node") {
16882
17317
  result.check = true;
16883
17318
  result[arg.slice(2)] = true;
16884
- } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--template" || arg === "--minutes" || arg === "--days" || arg === "--time" || arg === "--horizon" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app" || arg === "--tariff" || arg === "--class" || arg === "--level" || arg === "--ref" || arg === "--lang") {
17319
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--template" || arg === "--minutes" || arg === "--days" || arg === "--time" || arg === "--horizon" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app" || arg === "--tariff" || arg === "--class" || arg === "--level" || arg === "--ref" || arg === "--lang" || arg === "--id" || arg === "--uuid" || arg === "--intercom" || arg === "--page-size") {
16885
17320
  result[arg.slice(2)] = args[index + 1];
16886
17321
  index += 1;
16887
17322
  } else {
@@ -19083,6 +19518,14 @@ function mergeConfig(base, override) {
19083
19518
  ...(override.cloud?.providers || {}),
19084
19519
  },
19085
19520
  },
19521
+ domophones: {
19522
+ ...base.domophones,
19523
+ ...(override.domophones || {}),
19524
+ providers: {
19525
+ ...(base.domophones?.providers || {}),
19526
+ ...(override.domophones?.providers || {}),
19527
+ },
19528
+ },
19086
19529
  yandex: {
19087
19530
  ...base.yandex,
19088
19531
  ...(override.yandex || {}),
@@ -19176,6 +19619,12 @@ function sanitizeConfig(config) {
19176
19619
  next.skills = next.skills || {};
19177
19620
  next.skills.enabled = [...new Set([...(next.skills.enabled || []), "yandex-services"])];
19178
19621
  }
19622
+ if (next.domophones?.providers?.ufanet?.enabled) {
19623
+ next.toolsets = next.toolsets || {};
19624
+ next.toolsets.enabled = [...new Set([...(next.toolsets.enabled || []), "ufanet"])];
19625
+ next.skills = next.skills || {};
19626
+ next.skills.enabled = [...new Set([...(next.skills.enabled || []), "ufanet-intercom"])];
19627
+ }
19179
19628
  const localProfile = next.ai?.profiles?.local;
19180
19629
  if (localProfile?.provider === "iola") {
19181
19630
  if (!localProfile.runtime || localProfile.model === "iola-router-1b") {
@@ -19223,6 +19672,9 @@ function validateConfig(config) {
19223
19672
  if (config.cloud?.activeProvider && !["yandex-disk", "mailru-cloud"].includes(config.cloud.activeProvider)) {
19224
19673
  errors.push(`cloud.activeProvider неизвестен: ${config.cloud.activeProvider}`);
19225
19674
  }
19675
+ if (config.domophones?.activeProvider && !["ufanet", "domru", "rostelecom"].includes(config.domophones.activeProvider)) {
19676
+ errors.push(`domophones.activeProvider неизвестен: ${config.domophones.activeProvider}`);
19677
+ }
19226
19678
  for (const service of config.yandex?.enabledServices || []) {
19227
19679
  if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.enabledServices содержит неизвестный сервис: ${service}`);
19228
19680
  }
@@ -19243,6 +19695,7 @@ function configSchema() {
19243
19695
  toolsets: { available: Object.keys(TOOLSETS) },
19244
19696
  files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
19245
19697
  cloud: { providers: ["yandex-disk", "mailru-cloud"], root: CLOUD_DEFAULT_REMOTE_DIR },
19698
+ domophones: { providers: ["ufanet", "domru", "rostelecom"] },
19246
19699
  yandex: { services: Object.keys(YANDEX_CONNECTOR_SERVICES), statuses: ["ready", "research", "separate", "backlog"] },
19247
19700
  skills: { enabled: "array of skill names" },
19248
19701
  daemon: { host: "127.0.0.1", port: DAEMON_PORT },
package/wiki/Home.md CHANGED
@@ -14,6 +14,7 @@
14
14
  - geo-сценарии для жителей через Yandex Geocoder API;
15
15
  - Yandex Connector для пользовательских сервисов Яндекса;
16
16
  - Yandex Cloud Connector для геокодера, YandexGPT и deeplink Яндекс Go;
17
+ - Мой домофон Уфанет;
17
18
  - личные облачные диски: Яндекс Диск и Облако Mail.ru;
18
19
  - подключение публичного MCP-сервера.
19
20
 
@@ -35,6 +36,7 @@ iola ask "найди школу 29"
35
36
  - [Yandex Geocoder API key](Yandex-Geocoder-API-key)
36
37
  - [Yandex Cloud Connector](Yandex-Cloud-Connector)
37
38
  - [Yandex Connector](Yandex-Connector)
39
+ - [Мой домофон](Мой-домофон)
38
40
  - [Облачные диски](Облачные-диски)
39
41
  - [Скиллы для жителей](Скиллы-для-жителей)
40
42
  - [Локальный инструментальный агент](Локальный-инструментальный-агент)
@@ -10,6 +10,7 @@ Skills не подмешиваются в каждый запрос целико
10
10
  - `browser-agent` - когда запрос связан с сайтом, URL, страницей, скриншотом или браузером;
11
11
  - `local-model` - инструкции для локальных компактных моделей и tool-планирования;
12
12
  - `yandex-services` - когда запрос связан с Yandex Connector или Yandex Cloud Connector: Яндекс Диск, Почта, Календарь, Контакты, Телемост, Yandex ID, геокодер, YandexGPT или Яндекс Go deeplink;
13
+ - `ufanet-intercom` - когда запрос связан с домофоном Уфанет: открыть домофон, история звонков, камеры, записи;
13
14
  - `user-skills` - когда пользователь просит создать, включить, выключить или удалить собственный skill.
14
15
 
15
16
  Обычный диалог вроде `привет` не получает инструкции про слои, отчеты, файлы и браузер.
@@ -90,3 +91,16 @@ Toolset `yandex` включает локальные tools для пользов
90
91
  - Телемост: попытка прямого API и fallback через календарное событие, если API недоступен аккаунту.
91
92
 
92
93
  Опасные действия ограничены: отправка письма, удаление файлов, публикация ссылок, создание/изменение документов и создание/изменение/удаление событий требуют явного подтверждения в tool-вызове. Токены хранятся локально в `~/.iola/secrets.json`.
94
+
95
+ ## Ufanet toolset
96
+
97
+ Toolset `ufanet` включает tools для личного домофона Уфанет:
98
+
99
+ - `ufanet_status` - проверить подключение;
100
+ - `ufanet_intercoms` - список домофонов;
101
+ - `ufanet_open_intercom` - открыть домофон по ID;
102
+ - `ufanet_call_history` - история звонков;
103
+ - `ufanet_call_links` - ссылка на запись звонка по UUID;
104
+ - `ufanet_cameras` - камеры и RTSP-ссылки.
105
+
106
+ Открытие домофона требует явного подтверждения `confirm=true`. Договор, пароль и JWT хранятся локально и не выводятся в ответах.
@@ -18,7 +18,7 @@ iola master
18
18
  В мастере выберите:
19
19
 
20
20
  ```text
21
- 10. Yandex Cloud Connector - геокодинг и YandexGPT
21
+ 15. Yandex Cloud Connector - геокодинг и YandexGPT
22
22
  ```
23
23
 
24
24
  CLI не просто открывает консоль. Он печатает короткую инструкцию, что делать дальше, и только потом просит вставить ключи.
@@ -88,6 +88,21 @@ iola yandex token set
88
88
  iola yandex token delete
89
89
  ```
90
90
 
91
+ Мой домофон:
92
+
93
+ ```bash
94
+ iola ufanet setup
95
+ iola ufanet status
96
+ iola ufanet intercoms
97
+ iola ufanet open ID
98
+ iola ufanet history --limit 10
99
+ iola ufanet links UUID
100
+ iola ufanet cameras
101
+ iola ufanet delete
102
+ iola dom_ru
103
+ iola rostelecom
104
+ ```
105
+
91
106
  Локальная БД:
92
107
 
93
108
  ```bash
@@ -22,22 +22,28 @@ iola master
22
22
  4. Индекс локальных документов
23
23
  5. Браузерный модуль
24
24
 
25
+ Городские сервисы
26
+ 6. Открытые данные Йошкар-Олы
27
+ 7. Подключить городские данные к Codex
28
+ 8. Мой домофон Уфанет
29
+ 9. Мой домофон Дом.ру
30
+ 10. Мой домофон Ростелеком
31
+
25
32
  Локальный AI
26
- 6. IOLA локальная модель
27
- 7. Ollama
33
+ 11. IOLA локальная модель
34
+ 12. Ollama
28
35
 
29
36
  Российские AI и сервисы
30
- 8. GigaChat API
31
- 9. Yandex Connector
32
- 10. Yandex Cloud Connector
37
+ 13. GigaChat API
38
+ 14. Yandex Connector
39
+ 15. Yandex Cloud Connector
33
40
 
34
41
  Зарубежные AI
35
- 11. OpenAI API
36
- 12. OpenRouter API
42
+ 16. OpenAI API
43
+ 17. OpenRouter API
37
44
 
38
45
  Codex
39
- 13. Codex CLI
40
- 14. Подключение IOLA к Codex
46
+ 18. Codex CLI
41
47
  ```
42
48
 
43
49
  ## Базовая настройка
@@ -70,9 +76,39 @@ iola index folder ./docs
70
76
 
71
77
  Проверяет Playwright/Chromium runtime. Нужен для браузерного агента.
72
78
 
79
+ ## Городские сервисы
80
+
81
+ ### 6. Открытые данные Йошкар-Олы
82
+
83
+ Проверяет доступность городского API/MCP-шлюза `apiiola.yasg.ru`.
84
+
85
+ Этот пункт ничего не устанавливает локально. Он нужен, чтобы пользователь явно видел: CLI подключен к городским слоям открытых данных и может проверять школы, детские сады и другие подключенные городские сервисы через проверяемый gateway.
86
+
87
+ ### 7. Подключить городские данные к Codex
88
+
89
+ Добавляет MCP-сервер открытых данных Йошкар-Олы во внешний Codex CLI. Этот пункт нужен только тем, кто хочет, чтобы Codex CLI тоже видел городские данные как MCP-инструменты.
90
+
91
+ Для обычной работы `iola-cli` с городскими слоями этот пункт не обязателен: сам CLI уже ходит к городскому API/MCP-шлюзу напрямую.
92
+
93
+ ### 8. Мой домофон Уфанет
94
+
95
+ Подключает личный домофон Уфанет по номеру договора и паролю. Секреты хранятся только локально в `~/.iola/secrets.json`.
96
+
97
+ После настройки доступны команды `/ufanet`, `iola ufanet intercoms`, `iola ufanet history`, `iola ufanet cameras`, `iola ufanet open ID`. Открытие двери всегда требует явного подтверждения.
98
+
99
+ Инструкция: [Мой домофон](Мой-домофон).
100
+
101
+ ### 9. Мой домофон Дом.ру
102
+
103
+ Пункт-заготовка. Провайдер виден в городских сервисах, но API пока не реализован.
104
+
105
+ ### 10. Мой домофон Ростелеком
106
+
107
+ Пункт-заготовка. Провайдер виден в городских сервисах, но API пока не реализован.
108
+
73
109
  ## Локальный AI
74
110
 
75
- ### 6. IOLA локальная модель
111
+ ### 11. IOLA локальная модель
76
112
 
77
113
  Проверяет штатную локальную модель IOLA и готовит ее к работе через доступный runtime.
78
114
 
@@ -80,7 +116,7 @@ iola index folder ./docs
80
116
 
81
117
  После настройки локальную модель можно менять в интерактивном агенте через `/model`. Помимо штатной IOLA-модели можно выбрать установленную или рекомендуемую Ollama-модель, либо вручную ввести имя любой модели из библиотеки Ollama.
82
118
 
83
- ### 7. Ollama
119
+ ### 12. Ollama
84
120
 
85
121
  Опциональный локальный runtime для выбора сторонних моделей из библиотеки Ollama.
86
122
 
@@ -88,13 +124,13 @@ iola index folder ./docs
88
124
 
89
125
  ## Российские AI и сервисы
90
126
 
91
- ### 8. GigaChat API
127
+ ### 13. GigaChat API
92
128
 
93
129
  Настраивает профиль GigaChat и сохраняет authorization key локально у пользователя.
94
130
 
95
131
  Российский провайдер вызывается напрямую, без gateway/proxy.
96
132
 
97
- ### 9. Yandex Connector
133
+ ### 14. Yandex Connector
98
134
 
99
135
  Настраивает единый коннектор пользовательских сервисов Яндекса.
100
136
 
@@ -109,7 +145,7 @@ iola index folder ./docs
109
145
 
110
146
  Инструкция: [Yandex Connector](Yandex-Connector).
111
147
 
112
- ### 10. Yandex Cloud Connector
148
+ ### 15. Yandex Cloud Connector
113
149
 
114
150
  Настраивает Yandex Cloud Connector: геокодер и, при необходимости, YandexGPT.
115
151
 
@@ -123,13 +159,13 @@ iola index folder ./docs
123
159
 
124
160
  ## Зарубежные AI
125
161
 
126
- ### 11. OpenAI API
162
+ ### 16. OpenAI API
127
163
 
128
164
  Настраивает профиль OpenAI и сохраняет API-ключ локально у пользователя.
129
165
 
130
166
  После сохранения ключа мастер предлагает выбрать модель из доступного списка.
131
167
 
132
- ### 12. OpenRouter API
168
+ ### 17. OpenRouter API
133
169
 
134
170
  Настраивает профиль OpenRouter и сохраняет API-ключ локально у пользователя.
135
171
 
@@ -137,14 +173,10 @@ iola index folder ./docs
137
173
 
138
174
  ## Codex
139
175
 
140
- ### 13. Codex CLI
176
+ ### 18. Codex CLI
141
177
 
142
178
  Проверяет наличие Codex CLI и авторизации. Если Codex уже установлен и вход выполнен, пункт показывается как `готово`.
143
179
 
144
- ### 14. Подключение IOLA к Codex
145
-
146
- Добавляет MCP-сервер открытых данных Йошкар-Олы в Codex. Этот пункт можно запускать повторно для обновления подключения.
147
-
148
180
  ## Повторный запуск
149
181
 
150
182
  Если компонент уже настроен, его можно не выбирать. Если нужно переустановить или обновить компонент, выберите его номер вручную.
@@ -0,0 +1,68 @@
1
+ # Мой домофон
2
+
3
+ `iola-cli` поддерживает личные домофонные сервисы пользователя.
4
+
5
+ ## Уфанет
6
+
7
+ Рабочий провайдер:
8
+
9
+ ```bash
10
+ iola ufanet setup
11
+ iola ufanet status
12
+ iola ufanet intercoms
13
+ iola ufanet history
14
+ iola ufanet cameras
15
+ iola ufanet links UUID
16
+ iola ufanet open ID
17
+ iola ufanet delete
18
+ ```
19
+
20
+ При настройке нужны:
21
+
22
+ - номер договора Уфанет;
23
+ - пароль.
24
+
25
+ Они сохраняются только локально:
26
+
27
+ ```text
28
+ ~/.iola/secrets.json
29
+ ```
30
+
31
+ Можно также использовать переменные окружения:
32
+
33
+ ```text
34
+ UFANET_CONTRACT
35
+ UFANET_PASSWORD
36
+ ```
37
+
38
+ Доступные сценарии:
39
+
40
+ - показать доступные домофоны;
41
+ - открыть домофон по ID;
42
+ - показать историю звонков;
43
+ - получить ссылку на запись звонка по UUID;
44
+ - показать камеры;
45
+ - показать RTSP-ссылки камер;
46
+ - удалить локальное подключение.
47
+
48
+ Открытие двери всегда требует явного подтверждения пользователя.
49
+
50
+ ## Дом.ру
51
+
52
+ Команда-заготовка:
53
+
54
+ ```bash
55
+ iola dom_ru
56
+ ```
57
+
58
+ Провайдер пока в разработке. API/авторизация будут добавлены после исследования.
59
+
60
+ ## Ростелеком
61
+
62
+ Команда-заготовка:
63
+
64
+ ```bash
65
+ iola rostelecom
66
+ ```
67
+
68
+ Провайдер пока в разработке. API/авторизация будут добавлены после исследования.
@@ -403,6 +403,43 @@ iola yandex go open --from "Йошкар-Ола, Красноармейская
403
403
 
404
404
  Цена, повышенный спрос, детское кресло, назначение машины и отмена поездки через API ждут партнерский `clid/apikey` от Яндекса.
405
405
 
406
+ ## Мой домофон
407
+
408
+ Эти skills работают с личным домофоном пользователя.
409
+
410
+ ### Уфанет
411
+
412
+ Реализовано:
413
+
414
+ - проверить подключение Уфанет;
415
+ - показать список доступных домофонов;
416
+ - открыть выбранный домофон по ID;
417
+ - показать историю звонков;
418
+ - получить ссылку на запись звонка по UUID;
419
+ - показать камеры и RTSP-ссылки;
420
+ - удалить локальное подключение.
421
+
422
+ Открытие двери всегда требует явного подтверждения пользователя.
423
+
424
+ Команды:
425
+
426
+ ```bash
427
+ iola ufanet setup
428
+ iola ufanet intercoms
429
+ iola ufanet open ID
430
+ iola ufanet history
431
+ iola ufanet cameras
432
+ ```
433
+
434
+ ### Дом.ру и Ростелеком
435
+
436
+ Добавлены как направления `в разработке`:
437
+
438
+ ```bash
439
+ iola dom_ru
440
+ iola rostelecom
441
+ ```
442
+
406
443
  ## Yandex Connector backlog
407
444
 
408
445
  Эти сценарии зафиксированы для следующего этапа. Они не должны оформлять заказ, списывать деньги или нажимать финальную кнопку вместо пользователя.