@iola_adm/iola-cli 0.2.45 → 0.2.47

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,25 @@ 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 ufanet watch
219
+ iola ufanet notifications on
220
+ iola ufanet notifications off
221
+ iola dom_ru
222
+ iola rostelecom
223
+ ```
224
+
225
+ Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков, камеры/RTSP и уведомления о новых вызовах через опрос истории звонков, пока CLI открыт. Дом.ру и Ростелеком добавлены как видимые направления `в разработке`.
226
+
227
+ Инструкция: [Мой домофон](https://github.com/adm-iola/iola-cli/wiki/Мой-домофон).
228
+
210
229
  Зарубежные API-ключи:
211
230
 
212
231
  - OpenAI Platform: регистрация `https://platform.openai.com/`, ключи `https://platform.openai.com/api-keys`;
@@ -236,6 +255,7 @@ iola version --check
236
255
  - [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key)
237
256
  - [Yandex Cloud Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Cloud-Connector)
238
257
  - [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector)
258
+ - [Мой домофон](https://github.com/adm-iola/iola-cli/wiki/Мой-домофон)
239
259
  - [Облачные диски](https://github.com/adm-iola/iola-cli/wiki/Облачные-диски)
240
260
  - [Скиллы для жителей](https://github.com/adm-iola/iola-cli/wiki/Скиллы-для-жителей)
241
261
  - [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
@@ -259,6 +279,7 @@ iola version --check
259
279
  - AI-профили для IOLA local, Ollama, YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
260
280
  - Yandex Connector: единая точка подключения пользовательских сервисов Яндекса с локальным хранением OAuth-токенов;
261
281
  - Yandex Cloud Connector: геокодер, YandexGPT и deeplink маршрута Яндекс Go;
282
+ - Мой домофон Уфанет: домофоны, история звонков, записи, камеры и открытие двери после подтверждения;
262
283
  - локальный tool-agent для модели IOLA с tools открытых данных, файлов, браузера и сервисов Яндекса;
263
284
  - ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
264
285
  - личные облачные диски: Яндекс Диск и Облако Mail.ru для сохранения отчетов, backup и документов;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -0,0 +1,43 @@
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 watch` - показывать новые вызовы, пока CLI открыт.
27
+ - `iola ufanet notifications on|off|status` - включить, выключить или проверить настройку уведомлений.
28
+ - `iola ufanet links UUID` - ссылка на запись звонка.
29
+ - `iola ufanet cameras` - камеры.
30
+ - `iola ufanet delete` - удалить локальное подключение.
31
+
32
+ Безопасность:
33
+
34
+ - Для `ufanet_open_intercom` всегда нужен явный запрос пользователя и `confirm=true`.
35
+ - Если пользователь просит "открой домофон", но ID домофона неизвестен, сначала покажи список домофонов или попроси выбрать ID.
36
+ - Не открывай домофон по косвенному намерению вроде "кто там", "звонят", "посмотри".
37
+ - Не выводи договор, пароль, JWT-токен и другие секреты.
38
+ - Если Уфанет не подключен, скажи запустить `/master` и выбрать `Мой домофон Уфанет`, либо команду `iola ufanet setup`.
39
+
40
+ Дом.ру и Ростелеком:
41
+
42
+ - `/dom_ru` и `/rostelecom` пока являются заготовками "в разработке".
43
+ - Не обещай работу этих провайдеров, пока их 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, notifications: { enabled: false, intervalSeconds: 10, lastSeen: "" } },
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,10 @@ 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: "/ufanet watch", description: "уведомления о новых вызовах домофона" },
604
+ { command: "/dom_ru", description: "Мой домофон Дом.ру (в разработке)" },
605
+ { command: "/rostelecom", description: "Мой домофон Ростелеком (в разработке)" },
580
606
  { command: "/archive doctor", description: "архиватор" },
581
607
  { command: "/changes list", description: "подготовленные изменения" },
582
608
  { command: "/index status", description: "индекс документов" },
@@ -651,6 +677,10 @@ const COMMANDS = new Map([
651
677
  ["files", handleFiles],
652
678
  ["cloud", handleCloud],
653
679
  ["yandex", handleYandex],
680
+ ["ufanet", handleUfanet],
681
+ ["dom_ru", handleDomRu],
682
+ ["domru", handleDomRu],
683
+ ["rostelecom", handleRostelecom],
654
684
  ["archive", handleArchive],
655
685
  ["changes", handleChanges],
656
686
  ["import", handleImport],
@@ -801,6 +831,7 @@ async function showHelp() {
801
831
  iola browser status браузерный runtime
802
832
  iola cloud status облачные диски
803
833
  iola yandex status Yandex Connector
834
+ iola ufanet status Мой домофон Уфанет
804
835
  iola mcp status MCP-подключение
805
836
  iola doctor диагностика
806
837
  iola wiki документация
@@ -842,6 +873,9 @@ Usage:
842
873
  iola files status|mode|approvals|tree|read|search|write|patch
843
874
  iola cloud setup|status|ls|find|upload|download|share|save|backup
844
875
  iola yandex setup|menu|status|services|enable|disable|oauth-url|token
876
+ iola ufanet setup|status|intercoms|open|history|links|cameras|watch|notifications|delete
877
+ iola dom_ru Мой домофон Дом.ру (в разработке)
878
+ iola rostelecom Мой домофон Ростелеком (в разработке)
845
879
  iola archive doctor|list|test|extract|create|index
846
880
  iola changes list|show|apply|discard
847
881
  iola import file|folder
@@ -1581,6 +1615,10 @@ async function handleAgentLine(line, state) {
1581
1615
  files: ["files", args],
1582
1616
  archive: ["archive", args],
1583
1617
  yandex: ["yandex", args.length ? args : ["menu"]],
1618
+ ufanet: ["ufanet", args.length ? args : ["menu"]],
1619
+ dom_ru: ["dom_ru", args],
1620
+ domru: ["dom_ru", args],
1621
+ rostelecom: ["rostelecom", args],
1584
1622
  changes: ["changes", args],
1585
1623
  index: ["index", args],
1586
1624
  reports: ["reports", args],
@@ -3523,6 +3561,496 @@ async function handleYandex(args) {
3523
3561
  iola yandex backlog`);
3524
3562
  }
3525
3563
 
3564
+ async function handleDomRu() {
3565
+ console.log("Мой домофон Дом.ру: в разработке.");
3566
+ console.log("Пока доступна заготовка пункта в городских сервисах. API/авторизация будут добавлены после исследования провайдера.");
3567
+ }
3568
+
3569
+ async function handleRostelecom() {
3570
+ console.log("Мой домофон Ростелеком: в разработке.");
3571
+ console.log("Пока доступна заготовка пункта в городских сервисах. API/авторизация будут добавлены после исследования провайдера.");
3572
+ }
3573
+
3574
+ async function handleUfanet(args = []) {
3575
+ const [action = process.stdin.isTTY ? "menu" : "status", target, ...rest] = args;
3576
+ const options = parseOptions(rest);
3577
+
3578
+ if (action === "menu" || action === "choose") {
3579
+ await printUfanetMenu();
3580
+ return;
3581
+ }
3582
+
3583
+ if (action === "setup" || action === "connect" || action === "onboard") {
3584
+ await setupUfanetConnector();
3585
+ return;
3586
+ }
3587
+
3588
+ if (action === "status" || action === "doctor" || action === "check") {
3589
+ await printUfanetStatus({ check: action !== "status" || options.check });
3590
+ return;
3591
+ }
3592
+
3593
+ if (action === "intercoms" || action === "list" || action === "ls") {
3594
+ const rows = await ufanetGetIntercoms();
3595
+ printTable(rows, [["id", "ID"], ["name", "Название"], ["address", "Адрес"], ["role", "Роль"], ["blocked", "Блок"]]);
3596
+ return;
3597
+ }
3598
+
3599
+ if (action === "open") {
3600
+ const intercomId = target || options.id || options.intercom;
3601
+ if (!intercomId) throw new Error("Укажите ID домофона. Пример: iola ufanet open 123");
3602
+ const ok = options.yes || options.confirm || await confirm(`Открыть домофон Уфанет #${intercomId}? [y/N] `);
3603
+ if (!ok) {
3604
+ console.log("Открытие отменено.");
3605
+ return;
3606
+ }
3607
+ printKeyValue(await ufanetOpenIntercom(intercomId, { confirm: true }));
3608
+ return;
3609
+ }
3610
+
3611
+ if (action === "history" || action === "calls") {
3612
+ const rows = await ufanetGetCallHistory({ page: target || options.page || 1, pageSize: options.limit || options["page-size"] || 10 });
3613
+ printTable(rows.results || [], [["uuid", "UUID"], ["calledAt", "Когда"], ["address", "Адрес"], ["porch", "Подъезд"], ["flat", "Кв"]]);
3614
+ if (rows.count !== undefined) console.log(`Всего: ${rows.count}`);
3615
+ return;
3616
+ }
3617
+
3618
+ if (action === "watch" || action === "listen") {
3619
+ await watchUfanetCalls({ ...options, once: options.once, intervalSeconds: options.seconds || options.interval || target });
3620
+ return;
3621
+ }
3622
+
3623
+ if (action === "notifications" || action === "notify") {
3624
+ await handleUfanetNotifications([target, ...rest].filter(Boolean));
3625
+ return;
3626
+ }
3627
+
3628
+ if (action === "links" || action === "record" || action === "recording") {
3629
+ const uuid = target || options.uuid;
3630
+ if (!uuid) throw new Error("Укажите UUID звонка. Пример: iola ufanet links UUID");
3631
+ printKeyValue(await ufanetGetCallLinks(uuid));
3632
+ return;
3633
+ }
3634
+
3635
+ if (action === "cameras" || action === "camera") {
3636
+ const rows = await ufanetGetCameras();
3637
+ printTable(rows, [["number", "Номер"], ["title", "Название"], ["address", "Адрес"], ["type", "Тип"], ["rtspUrl", "RTSP"]]);
3638
+ return;
3639
+ }
3640
+
3641
+ if (action === "delete" || action === "disconnect" || action === "remove") {
3642
+ const ok = !process.stdin.isTTY || await askYesNo("Удалить локальные данные подключения Уфанет? [y/N] ", false);
3643
+ if (!ok) {
3644
+ console.log("Удаление отменено.");
3645
+ return;
3646
+ }
3647
+ await deleteUfanetConnector();
3648
+ return;
3649
+ }
3650
+
3651
+ throw new Error(`Команды ufanet:
3652
+ iola ufanet setup
3653
+ iola ufanet status|doctor
3654
+ iola ufanet intercoms
3655
+ iola ufanet open ID
3656
+ iola ufanet history [--limit 10]
3657
+ iola ufanet links UUID
3658
+ iola ufanet cameras
3659
+ iola ufanet watch [--seconds 10]
3660
+ iola ufanet notifications on|off|status
3661
+ iola ufanet delete`);
3662
+ }
3663
+
3664
+ async function printUfanetMenu() {
3665
+ const status = await getUfanetStatus();
3666
+ console.log("Мой домофон");
3667
+ printTable([
3668
+ { id: "ufanet", provider: "Уфанет", status: status.configured ? "готово" : "не настроено", command: "iola ufanet setup" },
3669
+ { id: "domru", provider: "Дом.ру", status: "в разработке", command: "iola dom_ru" },
3670
+ { id: "rostelecom", provider: "Ростелеком", status: "в разработке", command: "iola rostelecom" },
3671
+ ], [["id", "ID"], ["provider", "Провайдер"], ["status", "Статус"], ["command", "Команда"]]);
3672
+ console.log("");
3673
+ console.log("Команды Уфанет:");
3674
+ printTable([
3675
+ { command: "/ufanet status", action: "статус подключения" },
3676
+ { command: "/ufanet intercoms", action: "список доступных домофонов и их ID" },
3677
+ { command: "/ufanet open ID", action: "открыть домофон по ID, только после подтверждения" },
3678
+ { command: "/ufanet history", action: "история последних звонков" },
3679
+ { command: "/ufanet links UUID", action: "ссылка/превью записи звонка по UUID" },
3680
+ { command: "/ufanet cameras", action: "список камер и RTSP-ссылок, если доступны" },
3681
+ { command: "/ufanet watch", action: "показывать новые вызовы, пока CLI открыт" },
3682
+ { command: "/ufanet notifications on", action: "включить уведомления о вызовах в настройках" },
3683
+ { command: "/ufanet notifications off", action: "выключить уведомления о вызовах" },
3684
+ { command: "/ufanet delete", action: "удалить локальное подключение Уфанет" },
3685
+ ], [["command", "Команда"], ["action", "Что делает"]]);
3686
+ }
3687
+
3688
+ async function setupUfanetConnector() {
3689
+ console.log("Мой домофон Уфанет.");
3690
+ console.log("Нужны номер договора и пароль от сервиса Уфанет. Они сохраняются только локально в ~/.iola/secrets.json.");
3691
+ if (!process.stdin.isTTY) {
3692
+ console.log("Интерактивный ввод недоступен. Используйте env UFANET_CONTRACT и UFANET_PASSWORD или запустите iola ufanet setup в терминале.");
3693
+ return;
3694
+ }
3695
+ const secrets = await loadSecrets();
3696
+ const currentContract = secrets.ufanet?.contract || "";
3697
+ const contract = (await askText(`Номер договора${currentContract ? " [Enter - оставить]" : ""}: `)).trim() || currentContract;
3698
+ const password = (await askText(`Пароль Уфанет${secrets.ufanet?.password ? " [Enter - оставить]" : ""}: `)).trim() || secrets.ufanet?.password || "";
3699
+ if (!contract || !password) throw new Error("Для Уфанет нужны номер договора и пароль.");
3700
+ await saveUfanetConnectorSecrets({ contract, password });
3701
+ await enableUfanetConnector();
3702
+ console.log(`Уфанет сохранен локально: ${SECRETS_FILE}`);
3703
+ await printUfanetStatus({ check: true });
3704
+ }
3705
+
3706
+ async function saveUfanetConnectorSecrets({ contract, password }) {
3707
+ const secrets = await loadSecrets();
3708
+ secrets.ufanet = {
3709
+ ...(secrets.ufanet || {}),
3710
+ contract,
3711
+ password,
3712
+ updatedAt: new Date().toISOString(),
3713
+ };
3714
+ await saveSecrets(secrets);
3715
+ }
3716
+
3717
+ async function enableUfanetConnector() {
3718
+ const config = await loadConfig();
3719
+ await saveConfig({
3720
+ domophones: {
3721
+ ...(config.domophones || {}),
3722
+ activeProvider: "ufanet",
3723
+ providers: {
3724
+ ...(config.domophones?.providers || {}),
3725
+ ufanet: { ...(config.domophones?.providers?.ufanet || {}), enabled: true, updatedAt: new Date().toISOString() },
3726
+ domru: { ...(config.domophones?.providers?.domru || {}), enabled: false, status: "backlog" },
3727
+ rostelecom: { ...(config.domophones?.providers?.rostelecom || {}), enabled: false, status: "backlog" },
3728
+ },
3729
+ },
3730
+ toolsets: { ...(config.toolsets || {}), enabled: [...new Set([...(config.toolsets?.enabled || []), "ufanet"])] },
3731
+ skills: { ...(config.skills || {}), enabled: [...new Set([...(config.skills?.enabled || []), "ufanet-intercom"])] },
3732
+ });
3733
+ }
3734
+
3735
+ async function deleteUfanetConnector() {
3736
+ const secrets = await loadSecrets();
3737
+ delete secrets.ufanet;
3738
+ await saveSecrets(secrets);
3739
+ const config = await loadConfig();
3740
+ const enabledToolsets = (config.toolsets?.enabled || []).filter((item) => item !== "ufanet");
3741
+ const enabledSkills = (config.skills?.enabled || []).filter((item) => item !== "ufanet-intercom");
3742
+ await saveConfig({
3743
+ domophones: {
3744
+ ...(config.domophones || {}),
3745
+ activeProvider: config.domophones?.activeProvider === "ufanet" ? "" : config.domophones?.activeProvider || "",
3746
+ providers: {
3747
+ ...(config.domophones?.providers || {}),
3748
+ ufanet: { ...(config.domophones?.providers?.ufanet || {}), enabled: false },
3749
+ },
3750
+ },
3751
+ toolsets: { ...(config.toolsets || {}), enabled: enabledToolsets },
3752
+ skills: { ...(config.skills || {}), enabled: enabledSkills },
3753
+ });
3754
+ console.log("Подключение Уфанет удалено локально.");
3755
+ }
3756
+
3757
+ async function printUfanetStatus(options = {}) {
3758
+ const status = await getUfanetStatus();
3759
+ printKeyValue({
3760
+ configured: status.configured ? "yes" : "no",
3761
+ enabled: status.enabled ? "yes" : "no",
3762
+ contract: status.contract || "-",
3763
+ source: status.source || "-",
3764
+ notifications: status.notifications,
3765
+ intervalSeconds: status.intervalSeconds,
3766
+ });
3767
+ if (options.check) {
3768
+ if (!status.configured) {
3769
+ console.log("Уфанет: не настроен. Запустите: iola ufanet setup");
3770
+ return;
3771
+ }
3772
+ try {
3773
+ const rows = await ufanetGetIntercoms();
3774
+ console.log(`Уфанет: ok, домофонов: ${rows.length}`);
3775
+ } catch (error) {
3776
+ console.log(`Уфанет: ошибка проверки: ${error instanceof Error ? error.message : String(error)}`);
3777
+ }
3778
+ }
3779
+ }
3780
+
3781
+ async function getUfanetStatus() {
3782
+ const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3783
+ const contract = process.env.UFANET_CONTRACT || secrets.ufanet?.contract || "";
3784
+ const password = process.env.UFANET_PASSWORD || secrets.ufanet?.password || "";
3785
+ return {
3786
+ configured: Boolean(contract && password),
3787
+ enabled: Boolean(config.domophones?.providers?.ufanet?.enabled || (config.toolsets?.enabled || []).includes("ufanet")),
3788
+ contract: contract ? maskSecret(contract, 2) : "",
3789
+ source: contract && process.env.UFANET_CONTRACT ? "env" : contract ? "local" : "",
3790
+ notifications: config.domophones?.providers?.ufanet?.notifications?.enabled ? "on" : "off",
3791
+ intervalSeconds: config.domophones?.providers?.ufanet?.notifications?.intervalSeconds || 10,
3792
+ };
3793
+ }
3794
+
3795
+ async function handleUfanetNotifications(args = []) {
3796
+ const [action = "status", ...rest] = args;
3797
+ const options = parseOptions(rest);
3798
+ const normalized = String(action || "status").toLocaleLowerCase("ru-RU");
3799
+ if (["on", "enable", "start", "вкл", "включить"].includes(normalized)) {
3800
+ const seconds = Number(options.seconds || options.interval || options.wait || 10);
3801
+ await setUfanetNotifications(true, { intervalSeconds: seconds });
3802
+ console.log(`Уведомления Уфанет включены. Интервал проверки: ${normalizeUfanetPollInterval(seconds)} сек.`);
3803
+ console.log("Чтобы получать события в текущей CLI-сессии, запустите: /ufanet watch");
3804
+ return;
3805
+ }
3806
+ if (["off", "disable", "stop", "выкл", "выключить"].includes(normalized)) {
3807
+ await setUfanetNotifications(false);
3808
+ console.log("Уведомления Уфанет выключены.");
3809
+ return;
3810
+ }
3811
+ const status = await getUfanetStatus();
3812
+ console.log(`Уведомления Уфанет: ${status.notifications}, интервал ${status.intervalSeconds} сек.`);
3813
+ console.log("Команды: /ufanet notifications on, /ufanet notifications off, /ufanet watch");
3814
+ }
3815
+
3816
+ async function setUfanetNotifications(enabled, options = {}) {
3817
+ const config = await loadConfig();
3818
+ const current = config.domophones?.providers?.ufanet || {};
3819
+ const currentNotifications = current.notifications || {};
3820
+ await saveConfig({
3821
+ domophones: {
3822
+ ...(config.domophones || {}),
3823
+ providers: {
3824
+ ...(config.domophones?.providers || {}),
3825
+ ufanet: {
3826
+ ...current,
3827
+ notifications: {
3828
+ ...currentNotifications,
3829
+ enabled: Boolean(enabled),
3830
+ intervalSeconds: normalizeUfanetPollInterval(options.intervalSeconds || currentNotifications.intervalSeconds || 10),
3831
+ },
3832
+ },
3833
+ },
3834
+ },
3835
+ });
3836
+ }
3837
+
3838
+ async function updateUfanetLastSeen(lastSeen) {
3839
+ if (!lastSeen) return;
3840
+ const config = await loadConfig();
3841
+ const current = config.domophones?.providers?.ufanet || {};
3842
+ const currentNotifications = current.notifications || {};
3843
+ await saveConfig({
3844
+ domophones: {
3845
+ ...(config.domophones || {}),
3846
+ providers: {
3847
+ ...(config.domophones?.providers || {}),
3848
+ ufanet: {
3849
+ ...current,
3850
+ notifications: {
3851
+ ...currentNotifications,
3852
+ lastSeen,
3853
+ },
3854
+ },
3855
+ },
3856
+ },
3857
+ });
3858
+ }
3859
+
3860
+ async function watchUfanetCalls(options = {}) {
3861
+ const config = await loadConfig();
3862
+ const notificationConfig = config.domophones?.providers?.ufanet?.notifications || {};
3863
+ const intervalSeconds = normalizeUfanetPollInterval(options.intervalSeconds || notificationConfig.intervalSeconds || 10);
3864
+ const pageSize = Number(options.limit || options["page-size"] || 10);
3865
+ let lastSeen = notificationConfig.lastSeen || "";
3866
+ const initial = await ufanetGetCallHistory({ page: 1, pageSize });
3867
+ const initialRows = initial.results || [];
3868
+ if (!lastSeen && initialRows[0]) {
3869
+ lastSeen = ufanetCallKey(initialRows[0]);
3870
+ await updateUfanetLastSeen(lastSeen);
3871
+ }
3872
+ if (options.once) {
3873
+ printTable(initialRows, [["uuid", "UUID"], ["calledAt", "Когда"], ["address", "Адрес"], ["porch", "Подъезд"], ["flat", "Кв"]]);
3874
+ return;
3875
+ }
3876
+ console.log(`Уфанет: слежу за новыми вызовами каждые ${intervalSeconds} сек. Остановить: Ctrl+C.`);
3877
+ while (true) {
3878
+ await sleep(intervalSeconds * 1000);
3879
+ const history = await ufanetGetCallHistory({ page: 1, pageSize }).catch((error) => {
3880
+ console.error(`Уфанет: ошибка проверки вызовов: ${error instanceof Error ? error.message : String(error)}`);
3881
+ return { results: [] };
3882
+ });
3883
+ const rows = history.results || [];
3884
+ const fresh = [];
3885
+ for (const row of rows) {
3886
+ const key = ufanetCallKey(row);
3887
+ if (!key || key === lastSeen) break;
3888
+ fresh.push(row);
3889
+ }
3890
+ if (fresh.length === 0) continue;
3891
+ for (const row of fresh.reverse()) {
3892
+ console.log("");
3893
+ console.log("Новый вызов домофона Уфанет:");
3894
+ printKeyValue({
3895
+ uuid: row.uuid || "-",
3896
+ calledAt: row.calledAt || "-",
3897
+ address: row.address || "-",
3898
+ porch: row.porch || "-",
3899
+ flat: row.flat || "-",
3900
+ });
3901
+ }
3902
+ lastSeen = ufanetCallKey(rows[0]) || lastSeen;
3903
+ await updateUfanetLastSeen(lastSeen);
3904
+ }
3905
+ }
3906
+
3907
+ function normalizeUfanetPollInterval(value) {
3908
+ const seconds = Number(value || 10);
3909
+ if (!Number.isFinite(seconds)) return 10;
3910
+ return Math.max(5, Math.min(300, Math.round(seconds)));
3911
+ }
3912
+
3913
+ function ufanetCallKey(row = {}) {
3914
+ return String(row.uuid || `${row.calledAt || ""}|${row.address || ""}|${row.porch || ""}|${row.flat || ""}`);
3915
+ }
3916
+
3917
+ async function executeUfanetTool(tool, args = {}) {
3918
+ if (tool === "ufanet_status") return getUfanetStatus();
3919
+ if (tool === "ufanet_intercoms") return ufanetGetIntercoms();
3920
+ if (tool === "ufanet_open_intercom") return ufanetOpenIntercom(args.id || args.intercomId || args.intercom_id, args);
3921
+ if (tool === "ufanet_call_history") return ufanetGetCallHistory({ page: args.page || 1, pageSize: args.pageSize || args.page_size || args.limit || 10 });
3922
+ if (tool === "ufanet_call_links") return ufanetGetCallLinks(args.uuid || args.id);
3923
+ if (tool === "ufanet_cameras") return ufanetGetCameras();
3924
+ throw new Error(`Ufanet tool неизвестен: ${tool}`);
3925
+ }
3926
+
3927
+ async function ufanetCredentials() {
3928
+ const secrets = await loadSecrets();
3929
+ const contract = process.env.UFANET_CONTRACT || secrets.ufanet?.contract || "";
3930
+ const password = process.env.UFANET_PASSWORD || secrets.ufanet?.password || "";
3931
+ if (!contract || !password) throw new Error("Уфанет не подключен. Запустите: iola ufanet setup");
3932
+ return { contract, password, token: secrets.ufanet?.token || "", tokenUpdatedAt: secrets.ufanet?.tokenUpdatedAt || "" };
3933
+ }
3934
+
3935
+ async function ufanetEnsureToken(options = {}) {
3936
+ const credentials = await ufanetCredentials();
3937
+ if (credentials.token && !options.force) return credentials.token;
3938
+ const response = await fetch("https://dom.ufanet.ru/api/v1/auth/auth_by_contract/", {
3939
+ method: "POST",
3940
+ headers: { accept: "application/json", "content-type": "application/json" },
3941
+ body: JSON.stringify({ contract: credentials.contract, password: credentials.password }),
3942
+ signal: AbortSignal.timeout(30000),
3943
+ });
3944
+ const payload = await parseJsonResponse(response, "Уфанет авторизация");
3945
+ const token = payload?.token?.refresh || payload?.token?.access || payload?.refresh || payload?.access || "";
3946
+ if (!token) throw new Error("Уфанет не вернул JWT token.");
3947
+ if (!process.env.UFANET_CONTRACT && !process.env.UFANET_PASSWORD) {
3948
+ const secrets = await loadSecrets();
3949
+ secrets.ufanet = { ...(secrets.ufanet || {}), token, tokenUpdatedAt: new Date().toISOString() };
3950
+ await saveSecrets(secrets);
3951
+ }
3952
+ return token;
3953
+ }
3954
+
3955
+ async function ufanetRequest(method, apiPath, options = {}) {
3956
+ let token = await ufanetEnsureToken();
3957
+ let response = await fetch(new URL(apiPath, "https://dom.ufanet.ru/"), {
3958
+ method,
3959
+ headers: { accept: "application/json", "content-type": "application/json", Authorization: `JWT ${token}` },
3960
+ body: options.body ? JSON.stringify(options.body) : undefined,
3961
+ signal: AbortSignal.timeout(Number(options.timeout || 30000)),
3962
+ });
3963
+ if (response.status === 401) {
3964
+ token = await ufanetEnsureToken({ force: true });
3965
+ response = await fetch(new URL(apiPath, "https://dom.ufanet.ru/"), {
3966
+ method,
3967
+ headers: { accept: "application/json", "content-type": "application/json", Authorization: `JWT ${token}` },
3968
+ body: options.body ? JSON.stringify(options.body) : undefined,
3969
+ signal: AbortSignal.timeout(Number(options.timeout || 30000)),
3970
+ });
3971
+ }
3972
+ return parseJsonResponse(response, `Уфанет ${apiPath}`);
3973
+ }
3974
+
3975
+ async function parseJsonResponse(response, label) {
3976
+ const text = await response.text().catch(() => "");
3977
+ let payload = null;
3978
+ if (text) {
3979
+ try {
3980
+ payload = JSON.parse(text);
3981
+ } catch {
3982
+ payload = text;
3983
+ }
3984
+ }
3985
+ if (!response.ok) {
3986
+ const message = typeof payload === "string" ? payload.slice(0, 300) : JSON.stringify(payload).slice(0, 300);
3987
+ throw new Error(`${label}: ${response.status} ${response.statusText}${message ? ` ${message}` : ""}`);
3988
+ }
3989
+ return payload;
3990
+ }
3991
+
3992
+ async function ufanetGetIntercoms() {
3993
+ const payload = await ufanetRequest("GET", "api/v0/skud/shared/");
3994
+ return normalizeItems(payload).map((item) => ({
3995
+ id: item.id,
3996
+ name: item.custom_name || item.string_view || `Домофон ${item.id}`,
3997
+ address: item.string_view || "",
3998
+ role: item.role?.name || "",
3999
+ camera: item.camera || item.cctv_number || "",
4000
+ blocked: item.is_blocked ? "yes" : "no",
4001
+ disableButton: Boolean(item.disable_button),
4002
+ inactivityReason: item.inactivity_reason || "",
4003
+ raw: item,
4004
+ }));
4005
+ }
4006
+
4007
+ async function ufanetOpenIntercom(intercomId, options = {}) {
4008
+ if (!options.confirm) throw new Error("Для открытия домофона нужен аргумент confirm=true.");
4009
+ if (!intercomId) throw new Error("ID домофона обязателен.");
4010
+ const payload = await ufanetRequest("GET", `api/v0/skud/shared/${encodeURIComponent(intercomId)}/open/`, { timeout: 30000 });
4011
+ return { provider: "ufanet", status: payload?.result ? "opened" : "not-opened", id: Number(intercomId), result: Boolean(payload?.result) };
4012
+ }
4013
+
4014
+ async function ufanetGetCallHistory(options = {}) {
4015
+ const page = Math.max(1, Number(options.page || 1));
4016
+ const pageSize = Math.max(1, Math.min(100, Number(options.pageSize || 10)));
4017
+ const url = `api/v1/skuds/call-history/?page=${encodeURIComponent(page)}&page_size=${encodeURIComponent(pageSize)}`;
4018
+ const payload = await ufanetRequest("GET", url, { timeout: 30000 });
4019
+ return {
4020
+ count: payload?.count || 0,
4021
+ next: payload?.next || "",
4022
+ previous: payload?.previous || "",
4023
+ results: normalizeItems(payload?.results || []).map((item) => ({
4024
+ uuid: item.uuid,
4025
+ calledAt: item.called_at || item.calledAt || "",
4026
+ address: item.address || "",
4027
+ porch: item.porch || "",
4028
+ flat: item.flat || "",
4029
+ cameraNumber: item.camera_number || "",
4030
+ timezone: item.timezone || "",
4031
+ })),
4032
+ };
4033
+ }
4034
+
4035
+ async function ufanetGetCallLinks(uuid) {
4036
+ if (!uuid) throw new Error("UUID звонка обязателен.");
4037
+ const payload = await ufanetRequest("POST", "api/v1/cctv/history/", { body: { uuid: String(uuid) }, timeout: 30000 });
4038
+ return { provider: "ufanet", uuid: String(uuid), url: payload?.url || "", preview: payload?.preview || "" };
4039
+ }
4040
+
4041
+ async function ufanetGetCameras() {
4042
+ const payload = await ufanetRequest("GET", "api/v1/cctv", { timeout: 30000 });
4043
+ return normalizeItems(payload).map((item) => ({
4044
+ number: item.number || "",
4045
+ title: item.title || "",
4046
+ address: item.address || "",
4047
+ latitude: item.latitude,
4048
+ longitude: item.longitude,
4049
+ type: item.type || "",
4050
+ rtspUrl: item.servers?.domain && item.number && item.token_l ? `rtsp://${item.servers.domain}/${item.number}?token=${item.token_l}` : "",
4051
+ }));
4052
+ }
4053
+
3526
4054
  function printYandexServices(options = {}) {
3527
4055
  const rows = Object.entries(YANDEX_CONNECTOR_SERVICES)
3528
4056
  .filter(([, service]) => !options.status || service.status === options.status)
@@ -14545,7 +15073,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
14545
15073
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
14546
15074
  "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
14547
15075
  "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.",
15076
+ "Ufanet tools: ufanet_status {}, ufanet_intercoms {}, ufanet_open_intercom {id,confirm}, ufanet_call_history {page,limit}, ufanet_call_links {uuid}, ufanet_cameras {}.",
15077
+ "Опасные 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
15078
  "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
15079
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
14551
15080
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
@@ -14602,6 +15131,16 @@ function parseJsonObject(text) {
14602
15131
  return JSON.parse(match[0]);
14603
15132
  }
14604
15133
 
15134
+ function extractUfanetIntercomId(text) {
15135
+ return String(text || "").match(/(?:домофон|id|#|№)\s*(\d{1,10})/iu)?.[1]
15136
+ || String(text || "").match(/\b(\d{2,10})\b/u)?.[1]
15137
+ || "";
15138
+ }
15139
+
15140
+ function extractUuid(text) {
15141
+ 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] || "";
15142
+ }
15143
+
14605
15144
  function inferToolPlan(question, options = {}) {
14606
15145
  const normalized = question.toLocaleLowerCase("ru-RU");
14607
15146
  if (isCurrentDateTimeQuestion(normalized)) {
@@ -14635,6 +15174,26 @@ function inferToolPlan(question, options = {}) {
14635
15174
  if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
14636
15175
  return { steps: [{ tool: "yandex_identity_me", args: {} }] };
14637
15176
  }
15177
+ if (/(уфанет|домофон|домофонн|подъезд|звонк|камера|rtsp)/iu.test(normalized)) {
15178
+ if (/(дом\.?ру|dom\.?ru)/iu.test(normalized)) return { directAnswer: "Мой домофон Дом.ру пока в разработке. Сейчас реализован Уфанет: /ufanet." };
15179
+ if (/(ростелеком|rostelecom)/iu.test(normalized)) return { directAnswer: "Мой домофон Ростелеком пока в разработке. Сейчас реализован Уфанет: /ufanet." };
15180
+ if (/(открой|открыть|пусти|впусти|двер)/iu.test(normalized)) {
15181
+ const id = extractUfanetIntercomId(question);
15182
+ if (!id) return { directAnswer: "Для открытия домофона нужен ID. Посмотрите доступные домофоны командой /ufanet intercoms, затем: /ufanet open ID." };
15183
+ return { steps: [{ tool: "ufanet_open_intercom", args: { id, confirm: true } }] };
15184
+ }
15185
+ if (/(истори|звонк|кто\s+звонил|последн)/iu.test(normalized) && !/(ссылк|запис|видео)/iu.test(normalized)) {
15186
+ return { steps: [{ tool: "ufanet_call_history", args: { limit: 10 } }] };
15187
+ }
15188
+ if (/(ссылк|запис|видео|preview|превью)/iu.test(normalized)) {
15189
+ const uuid = extractUuid(question);
15190
+ if (!uuid) return { directAnswer: "Для ссылки на запись нужен UUID звонка. Сначала посмотрите историю: /ufanet history." };
15191
+ return { steps: [{ tool: "ufanet_call_links", args: { uuid } }] };
15192
+ }
15193
+ if (/(камер|rtsp|видео)/iu.test(normalized)) return { steps: [{ tool: "ufanet_cameras", args: {} }] };
15194
+ if (/(статус|подключ|аккаунт|договор)/iu.test(normalized)) return { steps: [{ tool: "ufanet_status", args: {} }] };
15195
+ return { steps: [{ tool: "ufanet_intercoms", args: {} }] };
15196
+ }
14638
15197
  if (/(яндекс|диск|облак)/iu.test(normalized)) {
14639
15198
  const diskPath = extractCloudPath(question) || CLOUD_DEFAULT_REMOTE_DIR;
14640
15199
  if (/(мест[оа]|сколько.*занято|сколько.*свобод|статус|инфо)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_info", args: {} }] };
@@ -15057,7 +15616,7 @@ function formatToolExecutionError(error, plan) {
15057
15616
  }
15058
15617
 
15059
15618
  function availableToolNames(options = {}) {
15060
- const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS]);
15619
+ const names = new Set([...LOCAL_TOOLS, ...YANDEX_TOOLS, ...UFANET_TOOLS, ...USER_SKILL_TOOLS]);
15061
15620
  if (options.files) {
15062
15621
  for (const tool of FILE_TOOLS) names.add(tool);
15063
15622
  }
@@ -15126,6 +15685,11 @@ async function executeToolPlan(plan, options = {}) {
15126
15685
  const result = await executeYandexTool(step.tool, step.args || {});
15127
15686
  current = Array.isArray(result) ? result : [result];
15128
15687
  outputs.push({ tool: step.tool, rows: current.length });
15688
+ } else if (UFANET_TOOLS.includes(step.tool)) {
15689
+ await assertPermission("externalApi");
15690
+ const result = await executeUfanetTool(step.tool, step.args || {});
15691
+ current = Array.isArray(result) ? result : [result];
15692
+ outputs.push({ tool: step.tool, rows: current.length });
15129
15693
  } else if (USER_SKILL_TOOLS.includes(step.tool)) {
15130
15694
  const result = await executeUserSkillTool(step.tool, step.args || {});
15131
15695
  current = Array.isArray(result) ? result : [result];
@@ -15281,6 +15845,12 @@ function formatToolResult(result, options) {
15281
15845
  return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
15282
15846
  }
15283
15847
  if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
15848
+ if (row.provider === "ufanet" && (row.status === "opened" || row.status === "not-opened")) return `Уфанет: домофон #${row.id} ${row.status === "opened" ? "открыт" : "не открылся"}.`;
15849
+ if (row.provider === "ufanet" && row.uuid && (row.url || row.preview)) return `Уфанет: запись звонка ${row.uuid}\nСсылка: ${row.url || "-"}\nПревью: ${row.preview || "-"}`;
15850
+ if (row.rtspUrl) return `Камера Уфанет ${row.title || row.number}: ${row.address || "-"}\nRTSP: ${row.rtspUrl}`;
15851
+ if (row.calledAt && row.uuid) return `Звонок Уфанет: ${row.calledAt}, ${row.address || "-"}, подъезд ${row.porch || "-"}, UUID ${row.uuid}`;
15852
+ if (row.id && (row.address || row.role || row.blocked !== undefined)) return `Домофон Уфанет #${row.id}: ${row.name || row.address || "-"}${row.blocked === "yes" ? " (заблокирован)" : ""}`;
15853
+ if (row.configured !== undefined && row.enabled !== undefined && row.contract !== undefined) return `Уфанет: ${row.configured ? "настроен" : "не настроен"}, ${row.enabled ? "включен" : "выключен"}${row.contract ? `, договор ${row.contract}` : ""}.`;
15284
15854
  if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
15285
15855
  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
15856
  }
@@ -16669,6 +17239,9 @@ async function onboard(args = []) {
16669
17239
  if (components.includes("policy")) await handlePolicy(["use", "analyst"]);
16670
17240
  if (components.includes("archive")) await ensureArchiveTool({ install: true });
16671
17241
  if (components.includes("city-data")) await checkHealth([]);
17242
+ if (components.includes("ufanet")) await setupUfanetConnector();
17243
+ if (components.includes("domru")) await handleDomRu();
17244
+ if (components.includes("rostelecom")) await handleRostelecom();
16672
17245
  if (components.includes("iola")) {
16673
17246
  await setupIolaLocal(["--yes"]);
16674
17247
  }
@@ -16757,14 +17330,17 @@ async function chooseOnboardComponents(status = null) {
16757
17330
  5: "browser",
16758
17331
  6: "city-data",
16759
17332
  7: "codex-mcp",
16760
- 8: "iola",
16761
- 9: "ollama",
16762
- 10: "gigachat",
16763
- 11: "yandex",
16764
- 12: "yandex-cloud",
16765
- 13: "openai",
16766
- 14: "openrouter",
16767
- 15: "codex",
17333
+ 8: "ufanet",
17334
+ 9: "domru",
17335
+ 10: "rostelecom",
17336
+ 11: "iola",
17337
+ 12: "ollama",
17338
+ 13: "gigachat",
17339
+ 14: "yandex",
17340
+ 15: "yandex-cloud",
17341
+ 16: "openai",
17342
+ 17: "openrouter",
17343
+ 18: "codex",
16768
17344
  };
16769
17345
  return [...selected].map((item) => map[item] || item).filter(Boolean);
16770
17346
  } finally {
@@ -16802,6 +17378,9 @@ async function getOnboardComponentStatus() {
16802
17378
  codex: Boolean(codexVersion !== "не найден" && readiness.codex),
16803
17379
  "codex-mcp": false,
16804
17380
  "city-data": cityDataHealth === "доступен",
17381
+ ufanet: Boolean((process.env.UFANET_CONTRACT && process.env.UFANET_PASSWORD) || (secrets.ufanet?.contract && secrets.ufanet?.password)),
17382
+ domru: false,
17383
+ rostelecom: false,
16805
17384
  archive: Boolean(archive),
16806
17385
  index: false,
16807
17386
  browser: browser.installed === "yes",
@@ -16826,40 +17405,43 @@ function onboardComponentGroups(status) {
16826
17405
  rows: [
16827
17406
  ["6", "city-data", "Открытые данные Йошкар-Олы", "API/MCP gateway доступен"],
16828
17407
  ["7", "codex-mcp", "Подключить городские данные к Codex", "MCP для Codex"],
17408
+ ["8", "ufanet", "Мой домофон Уфанет", "договор и пароль хранятся локально"],
17409
+ ["9", "domru", "Мой домофон Дом.ру", "в разработке"],
17410
+ ["10", "rostelecom", "Мой домофон Ростелеком", "в разработке"],
16829
17411
  ],
16830
17412
  },
16831
17413
  {
16832
17414
  title: "Локальный AI",
16833
17415
  rows: [
16834
- ["8", "iola", "IOLA локальная модель", "локальная модель найдена"],
16835
- ["9", "ollama", "Ollama", "опциональный локальный runtime"],
17416
+ ["11", "iola", "IOLA локальная модель", "локальная модель найдена"],
17417
+ ["12", "ollama", "Ollama", "опциональный локальный runtime"],
16836
17418
  ],
16837
17419
  },
16838
17420
  {
16839
17421
  title: "Российские AI и сервисы",
16840
17422
  rows: [
16841
- ["10", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
16842
- ["11", "yandex", "Yandex Connector", "Диск, Почта, Календарь, Контакты"],
16843
- ["12", "yandex-cloud", "Yandex Cloud Connector", "геокодинг и YandexGPT"],
17423
+ ["13", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
17424
+ ["14", "yandex", "Yandex Connector", "Диск, Почта, Календарь, Контакты"],
17425
+ ["15", "yandex-cloud", "Yandex Cloud Connector", "геокодинг и YandexGPT"],
16844
17426
  ],
16845
17427
  },
16846
17428
  {
16847
17429
  title: "Зарубежные AI",
16848
17430
  rows: [
16849
- ["13", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
16850
- ["14", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
17431
+ ["16", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
17432
+ ["17", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
16851
17433
  ],
16852
17434
  },
16853
17435
  {
16854
17436
  title: "Codex",
16855
17437
  rows: [
16856
- ["15", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
17438
+ ["18", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
16857
17439
  ],
16858
17440
  },
16859
17441
  ];
16860
17442
  return groups.map((group) => ({
16861
17443
  ...group,
16862
- rows: group.rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" })),
17444
+ rows: group.rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: key === "domru" || key === "rostelecom" ? "в разработке" : status[key] ? "готово" : "не настроено" })),
16863
17445
  }));
16864
17446
  }
16865
17447
 
@@ -16872,12 +17454,12 @@ function defaultOnboardSelection(status) {
16872
17454
  if (!status.workspace) defaults.push("1");
16873
17455
  if (!status.policy) defaults.push("2");
16874
17456
  if (!status.archive) defaults.push("3");
16875
- if (!status.iola) defaults.push("8");
17457
+ if (!status.iola) defaults.push("11");
16876
17458
  return defaults.length ? defaults : ["1", "2"];
16877
17459
  }
16878
17460
 
16879
17461
  function defaultOnboardComponents(status) {
16880
- const map = { 1: "workspace", 2: "policy", 3: "archive", 4: "index", 5: "browser", 6: "city-data", 7: "codex-mcp", 8: "iola", 9: "ollama", 10: "gigachat", 11: "yandex", 12: "yandex-cloud", 13: "openai", 14: "openrouter", 15: "codex" };
17462
+ 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" };
16881
17463
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
16882
17464
  }
16883
17465
 
@@ -16891,7 +17473,7 @@ function parseOptions(args) {
16891
17473
  } else if (arg === "--check" || arg === "--upgrade-node") {
16892
17474
  result.check = true;
16893
17475
  result[arg.slice(2)] = true;
16894
- } 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") {
17476
+ } 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" || arg === "--seconds" || arg === "--interval") {
16895
17477
  result[arg.slice(2)] = args[index + 1];
16896
17478
  index += 1;
16897
17479
  } else {
@@ -19093,6 +19675,14 @@ function mergeConfig(base, override) {
19093
19675
  ...(override.cloud?.providers || {}),
19094
19676
  },
19095
19677
  },
19678
+ domophones: {
19679
+ ...base.domophones,
19680
+ ...(override.domophones || {}),
19681
+ providers: {
19682
+ ...(base.domophones?.providers || {}),
19683
+ ...(override.domophones?.providers || {}),
19684
+ },
19685
+ },
19096
19686
  yandex: {
19097
19687
  ...base.yandex,
19098
19688
  ...(override.yandex || {}),
@@ -19186,6 +19776,12 @@ function sanitizeConfig(config) {
19186
19776
  next.skills = next.skills || {};
19187
19777
  next.skills.enabled = [...new Set([...(next.skills.enabled || []), "yandex-services"])];
19188
19778
  }
19779
+ if (next.domophones?.providers?.ufanet?.enabled) {
19780
+ next.toolsets = next.toolsets || {};
19781
+ next.toolsets.enabled = [...new Set([...(next.toolsets.enabled || []), "ufanet"])];
19782
+ next.skills = next.skills || {};
19783
+ next.skills.enabled = [...new Set([...(next.skills.enabled || []), "ufanet-intercom"])];
19784
+ }
19189
19785
  const localProfile = next.ai?.profiles?.local;
19190
19786
  if (localProfile?.provider === "iola") {
19191
19787
  if (!localProfile.runtime || localProfile.model === "iola-router-1b") {
@@ -19233,6 +19829,9 @@ function validateConfig(config) {
19233
19829
  if (config.cloud?.activeProvider && !["yandex-disk", "mailru-cloud"].includes(config.cloud.activeProvider)) {
19234
19830
  errors.push(`cloud.activeProvider неизвестен: ${config.cloud.activeProvider}`);
19235
19831
  }
19832
+ if (config.domophones?.activeProvider && !["ufanet", "domru", "rostelecom"].includes(config.domophones.activeProvider)) {
19833
+ errors.push(`domophones.activeProvider неизвестен: ${config.domophones.activeProvider}`);
19834
+ }
19236
19835
  for (const service of config.yandex?.enabledServices || []) {
19237
19836
  if (!YANDEX_CONNECTOR_SERVICES[service]) errors.push(`yandex.enabledServices содержит неизвестный сервис: ${service}`);
19238
19837
  }
@@ -19253,6 +19852,7 @@ function configSchema() {
19253
19852
  toolsets: { available: Object.keys(TOOLSETS) },
19254
19853
  files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
19255
19854
  cloud: { providers: ["yandex-disk", "mailru-cloud"], root: CLOUD_DEFAULT_REMOTE_DIR },
19855
+ domophones: { providers: ["ufanet", "domru", "rostelecom"] },
19256
19856
  yandex: { services: Object.keys(YANDEX_CONNECTOR_SERVICES), statuses: ["ready", "research", "separate", "backlog"] },
19257
19857
  skills: { enabled: "array of skill names" },
19258
19858
  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,18 @@ 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
+ Уведомления о новых вызовах доступны командой `iola ufanet watch`; включение/выключение настройки - `iola ufanet notifications on|off`.
107
+
108
+ Открытие домофона требует явного подтверждения `confirm=true`. Договор, пароль и JWT хранятся локально и не выводятся в ответах.
@@ -18,7 +18,7 @@ iola master
18
18
  В мастере выберите:
19
19
 
20
20
  ```text
21
- 12. Yandex Cloud Connector - геокодинг и YandexGPT
21
+ 15. Yandex Cloud Connector - геокодинг и YandexGPT
22
22
  ```
23
23
 
24
24
  CLI не просто открывает консоль. Он печатает короткую инструкцию, что делать дальше, и только потом просит вставить ключи.
@@ -88,6 +88,24 @@ 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 watch --seconds 10
102
+ iola ufanet notifications on
103
+ iola ufanet notifications off
104
+ iola ufanet delete
105
+ iola dom_ru
106
+ iola rostelecom
107
+ ```
108
+
91
109
  Локальная БД:
92
110
 
93
111
  ```bash
@@ -25,22 +25,25 @@ iola master
25
25
  Городские сервисы
26
26
  6. Открытые данные Йошкар-Олы
27
27
  7. Подключить городские данные к Codex
28
+ 8. Мой домофон Уфанет
29
+ 9. Мой домофон Дом.ру
30
+ 10. Мой домофон Ростелеком
28
31
 
29
32
  Локальный AI
30
- 8. IOLA локальная модель
31
- 9. Ollama
33
+ 11. IOLA локальная модель
34
+ 12. Ollama
32
35
 
33
36
  Российские AI и сервисы
34
- 10. GigaChat API
35
- 11. Yandex Connector
36
- 12. Yandex Cloud Connector
37
+ 13. GigaChat API
38
+ 14. Yandex Connector
39
+ 15. Yandex Cloud Connector
37
40
 
38
41
  Зарубежные AI
39
- 13. OpenAI API
40
- 14. OpenRouter API
42
+ 16. OpenAI API
43
+ 17. OpenRouter API
41
44
 
42
45
  Codex
43
- 15. Codex CLI
46
+ 18. Codex CLI
44
47
  ```
45
48
 
46
49
  ## Базовая настройка
@@ -87,9 +90,25 @@ iola index folder ./docs
87
90
 
88
91
  Для обычной работы `iola-cli` с городскими слоями этот пункт не обязателен: сам CLI уже ходит к городскому API/MCP-шлюзу напрямую.
89
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
+
90
109
  ## Локальный AI
91
110
 
92
- ### 8. IOLA локальная модель
111
+ ### 11. IOLA локальная модель
93
112
 
94
113
  Проверяет штатную локальную модель IOLA и готовит ее к работе через доступный runtime.
95
114
 
@@ -97,7 +116,7 @@ iola index folder ./docs
97
116
 
98
117
  После настройки локальную модель можно менять в интерактивном агенте через `/model`. Помимо штатной IOLA-модели можно выбрать установленную или рекомендуемую Ollama-модель, либо вручную ввести имя любой модели из библиотеки Ollama.
99
118
 
100
- ### 9. Ollama
119
+ ### 12. Ollama
101
120
 
102
121
  Опциональный локальный runtime для выбора сторонних моделей из библиотеки Ollama.
103
122
 
@@ -105,13 +124,13 @@ iola index folder ./docs
105
124
 
106
125
  ## Российские AI и сервисы
107
126
 
108
- ### 10. GigaChat API
127
+ ### 13. GigaChat API
109
128
 
110
129
  Настраивает профиль GigaChat и сохраняет authorization key локально у пользователя.
111
130
 
112
131
  Российский провайдер вызывается напрямую, без gateway/proxy.
113
132
 
114
- ### 11. Yandex Connector
133
+ ### 14. Yandex Connector
115
134
 
116
135
  Настраивает единый коннектор пользовательских сервисов Яндекса.
117
136
 
@@ -126,7 +145,7 @@ iola index folder ./docs
126
145
 
127
146
  Инструкция: [Yandex Connector](Yandex-Connector).
128
147
 
129
- ### 12. Yandex Cloud Connector
148
+ ### 15. Yandex Cloud Connector
130
149
 
131
150
  Настраивает Yandex Cloud Connector: геокодер и, при необходимости, YandexGPT.
132
151
 
@@ -140,13 +159,13 @@ iola index folder ./docs
140
159
 
141
160
  ## Зарубежные AI
142
161
 
143
- ### 13. OpenAI API
162
+ ### 16. OpenAI API
144
163
 
145
164
  Настраивает профиль OpenAI и сохраняет API-ключ локально у пользователя.
146
165
 
147
166
  После сохранения ключа мастер предлагает выбрать модель из доступного списка.
148
167
 
149
- ### 14. OpenRouter API
168
+ ### 17. OpenRouter API
150
169
 
151
170
  Настраивает профиль OpenRouter и сохраняет API-ключ локально у пользователя.
152
171
 
@@ -154,7 +173,7 @@ iola index folder ./docs
154
173
 
155
174
  ## Codex
156
175
 
157
- ### 15. Codex CLI
176
+ ### 18. Codex CLI
158
177
 
159
178
  Проверяет наличие Codex CLI и авторизации. Если Codex уже установлен и вход выполнен, пункт показывается как `готово`.
160
179
 
@@ -0,0 +1,82 @@
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 watch
18
+ iola ufanet notifications on
19
+ iola ufanet notifications off
20
+ iola ufanet notifications status
21
+ iola ufanet delete
22
+ ```
23
+
24
+ При настройке нужны:
25
+
26
+ - номер договора Уфанет;
27
+ - пароль.
28
+
29
+ Они сохраняются только локально:
30
+
31
+ ```text
32
+ ~/.iola/secrets.json
33
+ ```
34
+
35
+ Можно также использовать переменные окружения:
36
+
37
+ ```text
38
+ UFANET_CONTRACT
39
+ UFANET_PASSWORD
40
+ ```
41
+
42
+ Доступные сценарии:
43
+
44
+ - показать доступные домофоны;
45
+ - открыть домофон по ID;
46
+ - показать историю звонков;
47
+ - получить ссылку на запись звонка по UUID;
48
+ - показать камеры;
49
+ - показать RTSP-ссылки камер;
50
+ - получать уведомления о новых вызовах, пока CLI открыт;
51
+ - включить или выключить настройку уведомлений;
52
+ - удалить локальное подключение.
53
+
54
+ Открытие двери всегда требует явного подтверждения пользователя.
55
+
56
+ Уведомления сейчас работают через периодическую проверку истории звонков Уфанета:
57
+
58
+ ```bash
59
+ iola ufanet watch --seconds 10
60
+ ```
61
+
62
+ Остановка режима наблюдения: `Ctrl+C`. Команда `notifications on` сохраняет настройку, а `watch` запускает получение событий в текущей CLI-сессии.
63
+
64
+ ## Дом.ру
65
+
66
+ Команда-заготовка:
67
+
68
+ ```bash
69
+ iola dom_ru
70
+ ```
71
+
72
+ Провайдер пока в разработке. API/авторизация будут добавлены после исследования.
73
+
74
+ ## Ростелеком
75
+
76
+ Команда-заготовка:
77
+
78
+ ```bash
79
+ iola rostelecom
80
+ ```
81
+
82
+ Провайдер пока в разработке. API/авторизация будут добавлены после исследования.
@@ -403,6 +403,46 @@ 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
+ iola ufanet watch
433
+ iola ufanet notifications on
434
+ iola ufanet notifications off
435
+ ```
436
+
437
+ ### Дом.ру и Ростелеком
438
+
439
+ Добавлены как направления `в разработке`:
440
+
441
+ ```bash
442
+ iola dom_ru
443
+ iola rostelecom
444
+ ```
445
+
406
446
  ## Yandex Connector backlog
407
447
 
408
448
  Эти сценарии зафиксированы для следующего этапа. Они не должны оформлять заказ, списывать деньги или нажимать финальную кнопку вместо пользователя.