@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 +21 -0
- package/package.json +1 -1
- package/skills/ufanet-intercom/SKILL.md +43 -0
- package/src/cli.js +623 -23
- package/wiki/Home.md +2 -0
- package/wiki/Skills-/320/270-toolsets.md +16 -0
- package/wiki/Yandex-Cloud-Connector.md +1 -1
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +18 -0
- package/wiki//320/234/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +35 -16
- package/wiki//320/234/320/276/320/271-/320/264/320/276/320/274/320/276/321/204/320/276/320/275.md +82 -0
- package/wiki//320/241/320/272/320/270/320/273/320/273/321/213-/320/264/320/273/321/217-/320/266/320/270/321/202/320/265/320/273/320/265/320/271.md +40 -0
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
|
@@ -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
|
-
"
|
|
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: "
|
|
16761
|
-
9: "
|
|
16762
|
-
10: "
|
|
16763
|
-
11: "
|
|
16764
|
-
12: "
|
|
16765
|
-
13: "
|
|
16766
|
-
14: "
|
|
16767
|
-
15: "
|
|
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
|
-
["
|
|
16835
|
-
["
|
|
17416
|
+
["11", "iola", "IOLA локальная модель", "локальная модель найдена"],
|
|
17417
|
+
["12", "ollama", "Ollama", "опциональный локальный runtime"],
|
|
16836
17418
|
],
|
|
16837
17419
|
},
|
|
16838
17420
|
{
|
|
16839
17421
|
title: "Российские AI и сервисы",
|
|
16840
17422
|
rows: [
|
|
16841
|
-
["
|
|
16842
|
-
["
|
|
16843
|
-
["
|
|
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
|
-
["
|
|
16850
|
-
["
|
|
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
|
-
["
|
|
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("
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
33
|
+
11. IOLA локальная модель
|
|
34
|
+
12. Ollama
|
|
32
35
|
|
|
33
36
|
Российские AI и сервисы
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
13. GigaChat API
|
|
38
|
+
14. Yandex Connector
|
|
39
|
+
15. Yandex Cloud Connector
|
|
37
40
|
|
|
38
41
|
Зарубежные AI
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
16. OpenAI API
|
|
43
|
+
17. OpenRouter API
|
|
41
44
|
|
|
42
45
|
Codex
|
|
43
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
127
|
+
### 13. GigaChat API
|
|
109
128
|
|
|
110
129
|
Настраивает профиль GigaChat и сохраняет authorization key локально у пользователя.
|
|
111
130
|
|
|
112
131
|
Российский провайдер вызывается напрямую, без gateway/proxy.
|
|
113
132
|
|
|
114
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
162
|
+
### 16. OpenAI API
|
|
144
163
|
|
|
145
164
|
Настраивает профиль OpenAI и сохраняет API-ключ локально у пользователя.
|
|
146
165
|
|
|
147
166
|
После сохранения ключа мастер предлагает выбрать модель из доступного списка.
|
|
148
167
|
|
|
149
|
-
###
|
|
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
|
-
###
|
|
176
|
+
### 18. Codex CLI
|
|
158
177
|
|
|
159
178
|
Проверяет наличие Codex CLI и авторизации. Если Codex уже установлен и вход выполнен, пункт показывается как `готово`.
|
|
160
179
|
|
package/wiki//320/234/320/276/320/271-/320/264/320/276/320/274/320/276/321/204/320/276/320/275.md
ADDED
|
@@ -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
|
Эти сценарии зафиксированы для следующего этапа. Они не должны оформлять заказ, списывать деньги или нажимать финальную кнопку вместо пользователя.
|