@iola_adm/iola-cli 0.2.48 → 0.2.50
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 +4 -1
- package/package.json +1 -1
- package/skills/ufanet-intercom/SKILL.md +9 -4
- package/src/cli.js +314 -42
- package/wiki/Skills-/320/270-toolsets.md +4 -2
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +3 -0
- package/wiki//320/234/320/276/320/271-/320/264/320/276/320/274/320/276/321/204/320/276/320/275.md +28 -2
- 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 +5 -1
package/README.md
CHANGED
|
@@ -210,12 +210,15 @@ Yandex tools уже доступны: профиль Yandex ID, расширен
|
|
|
210
210
|
Мой домофон:
|
|
211
211
|
|
|
212
212
|
```bash
|
|
213
|
+
iola ufanet
|
|
213
214
|
iola ufanet setup
|
|
214
215
|
iola ufanet intercoms
|
|
215
216
|
iola ufanet open
|
|
216
217
|
iola ufanet open ID
|
|
217
218
|
iola ufanet history
|
|
218
219
|
iola ufanet cameras
|
|
220
|
+
iola ufanet camera open НОМЕР
|
|
221
|
+
iola ufanet camera snapshot НОМЕР
|
|
219
222
|
iola ufanet watch
|
|
220
223
|
iola ufanet notifications on
|
|
221
224
|
iola ufanet notifications off
|
|
@@ -223,7 +226,7 @@ iola dom_ru
|
|
|
223
226
|
iola rostelecom
|
|
224
227
|
```
|
|
225
228
|
|
|
226
|
-
Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков,
|
|
229
|
+
`/ufanet` в интерактивном CLI открывает отдельное меню домофона с выбором действия цифрой. Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков, безопасный список камер без вывода RTSP-токенов, открытие видео во внешнем плеере, локальный снимок камеры через ffmpeg и уведомления о новых вызовах через опрос истории звонков, пока CLI открыт. Команда `открой домофон` открывает единственный домофон сразу; если домофонов несколько, CLI просит выбрать адрес цифрой. При уведомлении о вызове можно ответить `да`, `да открой` или `нет`. Дом.ру и Ростелеком добавлены как видимые направления `в разработке`.
|
|
227
230
|
|
|
228
231
|
Инструкция: [Мой домофон](https://github.com/adm-iola/iola-cli/wiki/Мой-домофон).
|
|
229
232
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ufanet-intercom
|
|
3
|
-
description: Мой домофон Уфанет: список домофонов, открытие двери после подтверждения, история звонков, уведомления о новых вызовах, записи звонков,
|
|
3
|
+
description: Мой домофон Уфанет: список домофонов, открытие двери после подтверждения, история звонков, уведомления о новых вызовах, записи звонков, камеры, открытие видео и локальные снимки.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Используй этот skill, когда пользователь явно просит работать с домофоном Уфанет: открыть домофон, показать доступные домофоны, посмотреть историю звонков, включить/выключить уведомления о вызовах, получить запись звонка, показать
|
|
6
|
+
Используй этот skill, когда пользователь явно просит работать с домофоном Уфанет: открыть домофон, показать доступные домофоны, посмотреть историю звонков, включить/выключить уведомления о вызовах, получить запись звонка, показать камеры, открыть видео или сохранить снимок с камеры.
|
|
7
7
|
|
|
8
8
|
Не смешивай Уфанет с Яндекс-сервисами, городскими открытыми слоями и локальными файлами. Это отдельный личный сервис пользователя.
|
|
9
9
|
|
|
@@ -14,7 +14,9 @@ description: Мой домофон Уфанет: список домофонов
|
|
|
14
14
|
- `ufanet_open_intercom` - открыть домофон по ID; если ID не указан, CLI сам получает список домофонов.
|
|
15
15
|
- `ufanet_call_history` - показать историю звонков домофона.
|
|
16
16
|
- `ufanet_call_links` - получить ссылку на запись/превью звонка по UUID.
|
|
17
|
-
- `ufanet_cameras` - показать доступные камеры
|
|
17
|
+
- `ufanet_cameras` - показать доступные камеры без вывода RTSP-токенов.
|
|
18
|
+
- `ufanet_camera_open` - открыть видеопоток камеры во внешнем плеере.
|
|
19
|
+
- `ufanet_camera_snapshot` - сохранить один кадр с камеры в локальный файл через ffmpeg.
|
|
18
20
|
|
|
19
21
|
Команды CLI:
|
|
20
22
|
|
|
@@ -28,6 +30,8 @@ description: Мой домофон Уфанет: список домофонов
|
|
|
28
30
|
- `iola ufanet notifications on|off|status` - включить, выключить или проверить настройку уведомлений.
|
|
29
31
|
- `iola ufanet links UUID` - ссылка на запись звонка.
|
|
30
32
|
- `iola ufanet cameras` - камеры.
|
|
33
|
+
- `iola ufanet camera open НОМЕР` - открыть видео камеры во внешнем плеере.
|
|
34
|
+
- `iola ufanet camera snapshot НОМЕР` - сохранить снимок камеры в локальный `.jpg`.
|
|
31
35
|
- `iola ufanet delete` - удалить локальное подключение.
|
|
32
36
|
|
|
33
37
|
Безопасность:
|
|
@@ -36,7 +40,8 @@ description: Мой домофон Уфанет: список домофонов
|
|
|
36
40
|
- Если пользователь просит "открой домофон", а ID не указан, CLI должен сам получить список домофонов. Если домофон один - открыть его. Если домофонов несколько - показать адреса с цифрами и ждать выбор цифрой; выбранная цифра считается подтверждением.
|
|
37
41
|
- Если включены уведомления и пришел вызов, в сообщении должен быть вопрос "Открыть?". Ответы `да`, `да открой`, `открой`, `ок` открывают сопоставленный домофон. Ответы `нет`, `отмена` отменяют.
|
|
38
42
|
- Не открывай домофон по косвенному намерению вроде "кто там", "звонят", "посмотри".
|
|
39
|
-
- Не выводи договор, пароль, JWT
|
|
43
|
+
- Не выводи договор, пароль, JWT-токен, RTSP-ссылки с токенами и другие секреты.
|
|
44
|
+
- В истории звонков не показывай UUID без явной технической необходимости.
|
|
40
45
|
- Если Уфанет не подключен, скажи запустить `/master` и выбрать `Мой домофон Уфанет`, либо команду `iola ufanet setup`.
|
|
41
46
|
|
|
42
47
|
Дом.ру и Ростелеком:
|
package/src/cli.js
CHANGED
|
@@ -152,7 +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
|
+
const UFANET_TOOLS = ["ufanet_status", "ufanet_intercoms", "ufanet_open_intercom", "ufanet_call_history", "ufanet_call_links", "ufanet_cameras", "ufanet_camera_open", "ufanet_camera_snapshot"];
|
|
156
156
|
const YANDEX_TOOLS = [
|
|
157
157
|
"yandex_identity_me",
|
|
158
158
|
"yandex_disk_info",
|
|
@@ -1282,11 +1282,15 @@ async function startAgentRawInput() {
|
|
|
1282
1282
|
|
|
1283
1283
|
async function handleAgentLine(line, state) {
|
|
1284
1284
|
if (!line.startsWith("/")) {
|
|
1285
|
+
if (/^\d{1,2}$/u.test(line.trim()) && (state.pendingAction?.type === "ufanet_menu" || state.lastCommand?.command === "ufanet")) {
|
|
1286
|
+
state.pendingAction = state.pendingAction?.type === "ufanet_menu" ? state.pendingAction : buildUfanetMenuPendingAction();
|
|
1287
|
+
}
|
|
1285
1288
|
const pendingAnswer = await handlePendingAgentAction(line, state);
|
|
1286
1289
|
const answer = pendingAnswer || await aiAsk(state.rawMode ? [line, "--quiet"] : [line], { history: state.history, state });
|
|
1287
1290
|
state.history.push({ role: "user", content: line });
|
|
1288
1291
|
state.history.push({ role: "assistant", content: answer });
|
|
1289
1292
|
if (state.rawMode) state.pendingOutput = answer;
|
|
1293
|
+
else if (pendingAnswer) printAiAnswer(pendingAnswer);
|
|
1290
1294
|
return false;
|
|
1291
1295
|
}
|
|
1292
1296
|
|
|
@@ -1308,6 +1312,10 @@ async function handleAgentLine(line, state) {
|
|
|
1308
1312
|
}
|
|
1309
1313
|
|
|
1310
1314
|
if (command === "help") {
|
|
1315
|
+
if (state.pendingAction?.type === "ufanet_menu") {
|
|
1316
|
+
await printUfanetMenu({ agentState: state, pending: true });
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1311
1319
|
printAgentHelp();
|
|
1312
1320
|
return false;
|
|
1313
1321
|
}
|
|
@@ -1942,6 +1950,21 @@ async function handlePendingAgentAction(line, state) {
|
|
|
1942
1950
|
const result = await ufanetOpenIntercom(action.id, { confirm: true });
|
|
1943
1951
|
return formatUfanetOpenResult(result, action);
|
|
1944
1952
|
}
|
|
1953
|
+
if (action.type === "ufanet_menu") {
|
|
1954
|
+
const number = Number(text.match(/^\s*(\d{1,2})\s*$/u)?.[1] || 0);
|
|
1955
|
+
if (number === 0) {
|
|
1956
|
+
state.pendingAction = null;
|
|
1957
|
+
return "Выход из меню домофона.";
|
|
1958
|
+
}
|
|
1959
|
+
if (!number || number < 0 || number > action.items.length) {
|
|
1960
|
+
state.pendingAction = buildUfanetMenuPendingAction();
|
|
1961
|
+
return await formatUfanetMenu({ compact: true });
|
|
1962
|
+
}
|
|
1963
|
+
const item = action.items[number - 1];
|
|
1964
|
+
state.pendingAction = null;
|
|
1965
|
+
const answer = await executeUfanetMenuItem(item, state);
|
|
1966
|
+
return answer || "";
|
|
1967
|
+
}
|
|
1945
1968
|
return "";
|
|
1946
1969
|
}
|
|
1947
1970
|
|
|
@@ -3714,8 +3737,25 @@ async function handleUfanet(args = [], agentState = null) {
|
|
|
3714
3737
|
const [action = process.stdin.isTTY ? "menu" : "status", target, ...rest] = args;
|
|
3715
3738
|
const options = parseOptions(rest);
|
|
3716
3739
|
|
|
3717
|
-
if (action === "menu" || action === "choose") {
|
|
3718
|
-
await printUfanetMenu();
|
|
3740
|
+
if (action === "menu" || action === "choose" || action === "help") {
|
|
3741
|
+
await printUfanetMenu({ agentState, pending: action !== "help" });
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
if (/^\d{1,2}$/u.test(String(action))) {
|
|
3746
|
+
const number = Number(action);
|
|
3747
|
+
const items = getUfanetMenuItems();
|
|
3748
|
+
if (number === 0) {
|
|
3749
|
+
console.log("Выход из меню домофона.");
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
const item = items[number - 1];
|
|
3753
|
+
if (!item) {
|
|
3754
|
+
console.log(formatUfanetMenu({ compact: true }));
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
const answer = await executeUfanetMenuItem(item, agentState);
|
|
3758
|
+
if (answer) console.log(answer);
|
|
3719
3759
|
return;
|
|
3720
3760
|
}
|
|
3721
3761
|
|
|
@@ -3752,14 +3792,18 @@ async function handleUfanet(args = [], agentState = null) {
|
|
|
3752
3792
|
}
|
|
3753
3793
|
|
|
3754
3794
|
if (action === "history" || action === "calls") {
|
|
3755
|
-
const
|
|
3756
|
-
|
|
3795
|
+
const historyOptions = parseOptions([target, ...rest].filter(Boolean));
|
|
3796
|
+
const page = target && !String(target).startsWith("--") ? target : historyOptions.page || 1;
|
|
3797
|
+
const rows = await ufanetGetCallHistory({ page, pageSize: historyOptions.limit || historyOptions["page-size"] || 10 });
|
|
3798
|
+
printTable(formatUfanetCallRows(rows.results || []), [["index", "#"], ["calledAt", "Когда"], ["address", "Адрес"], ["porch", "Подъезд"]]);
|
|
3757
3799
|
if (rows.count !== undefined) console.log(`Всего: ${rows.count}`);
|
|
3758
3800
|
return;
|
|
3759
3801
|
}
|
|
3760
3802
|
|
|
3761
3803
|
if (action === "watch" || action === "listen") {
|
|
3762
|
-
|
|
3804
|
+
const watchOptions = parseOptions([target, ...rest].filter(Boolean));
|
|
3805
|
+
const intervalTarget = target && !String(target).startsWith("--") ? target : "";
|
|
3806
|
+
await watchUfanetCalls({ ...watchOptions, once: watchOptions.once, intervalSeconds: watchOptions.seconds || watchOptions.interval || intervalTarget });
|
|
3763
3807
|
return;
|
|
3764
3808
|
}
|
|
3765
3809
|
|
|
@@ -3775,9 +3819,30 @@ async function handleUfanet(args = [], agentState = null) {
|
|
|
3775
3819
|
return;
|
|
3776
3820
|
}
|
|
3777
3821
|
|
|
3778
|
-
if (action === "cameras"
|
|
3822
|
+
if (action === "cameras") {
|
|
3779
3823
|
const rows = await ufanetGetCameras();
|
|
3780
|
-
|
|
3824
|
+
console.log(formatUfanetCameraList(rows));
|
|
3825
|
+
return;
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
if (action === "camera") {
|
|
3829
|
+
const subaction = target || "list";
|
|
3830
|
+
const selector = rest[0] || options.number || options.id || options.camera || options._?.[0];
|
|
3831
|
+
if (subaction === "list" || subaction === "ls" || subaction === "cameras") {
|
|
3832
|
+
console.log(formatUfanetCameraList(await ufanetGetCameras()));
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
if (subaction === "open" || subaction === "view" || subaction === "play") {
|
|
3836
|
+
const result = await ufanetOpenCamera(selector || 1);
|
|
3837
|
+
console.log(result.message);
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
if (subaction === "snapshot" || subaction === "screen" || subaction === "screenshot" || subaction === "photo" || subaction === "frame") {
|
|
3841
|
+
const result = await ufanetCameraSnapshot(selector || 1);
|
|
3842
|
+
console.log(`Снимок с камеры сохранен: ${result.file}`);
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
throw new Error("Команды камеры: iola ufanet camera list | open НОМЕР | snapshot НОМЕР");
|
|
3781
3846
|
return;
|
|
3782
3847
|
}
|
|
3783
3848
|
|
|
@@ -3799,33 +3864,94 @@ async function handleUfanet(args = [], agentState = null) {
|
|
|
3799
3864
|
iola ufanet history [--limit 10]
|
|
3800
3865
|
iola ufanet links UUID
|
|
3801
3866
|
iola ufanet cameras
|
|
3867
|
+
iola ufanet camera open НОМЕР
|
|
3868
|
+
iola ufanet camera snapshot НОМЕР
|
|
3802
3869
|
iola ufanet watch [--seconds 10]
|
|
3803
3870
|
iola ufanet notifications on|off|status
|
|
3804
3871
|
iola ufanet delete`);
|
|
3805
3872
|
}
|
|
3806
3873
|
|
|
3807
|
-
async function printUfanetMenu() {
|
|
3874
|
+
async function printUfanetMenu(options = {}) {
|
|
3875
|
+
if (options.agentState && options.pending) {
|
|
3876
|
+
options.agentState.pendingAction = buildUfanetMenuPendingAction();
|
|
3877
|
+
}
|
|
3878
|
+
console.log(await formatUfanetMenu());
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
async function formatUfanetMenu(options = {}) {
|
|
3808
3882
|
const status = await getUfanetStatus();
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
{
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
{
|
|
3826
|
-
{
|
|
3827
|
-
{
|
|
3828
|
-
|
|
3883
|
+
const lines = [
|
|
3884
|
+
"Мой домофон",
|
|
3885
|
+
`Уфанет: ${status.configured ? "готово" : "не настроено"}; уведомления: ${status.notifications}.`,
|
|
3886
|
+
"Дом.ру: в разработке. Ростелеком: в разработке.",
|
|
3887
|
+
"",
|
|
3888
|
+
"Выберите действие:",
|
|
3889
|
+
...getUfanetMenuItems().map((item) => `${item.number}. ${item.title}`),
|
|
3890
|
+
"0. Назад",
|
|
3891
|
+
"",
|
|
3892
|
+
"Введите цифру или команду вида `/ufanet 3`. Пока открыт этот раздел, `/help` показывает справку по домофону.",
|
|
3893
|
+
];
|
|
3894
|
+
return options.compact ? lines.slice(4).join("\n") : lines.join("\n");
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
function getUfanetMenuItems() {
|
|
3898
|
+
return [
|
|
3899
|
+
{ number: 1, key: "open", title: "Открыть домофон" },
|
|
3900
|
+
{ number: 2, key: "intercoms", title: "Показать мои домофоны" },
|
|
3901
|
+
{ number: 3, key: "history", title: "История звонков" },
|
|
3902
|
+
{ number: 4, key: "notifications-on", title: "Включить уведомления о вызовах" },
|
|
3903
|
+
{ number: 5, key: "notifications-off", title: "Выключить уведомления" },
|
|
3904
|
+
{ number: 6, key: "notifications-status", title: "Статус уведомлений" },
|
|
3905
|
+
{ number: 7, key: "cameras", title: "Камеры" },
|
|
3906
|
+
{ number: 8, key: "status", title: "Статус подключения" },
|
|
3907
|
+
{ number: 9, key: "delete", title: "Удалить подключение Уфанет" },
|
|
3908
|
+
];
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
function buildUfanetMenuPendingAction() {
|
|
3912
|
+
return { type: "ufanet_menu", items: getUfanetMenuItems(), createdAt: new Date().toISOString() };
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
async function executeUfanetMenuItem(item, agentState = null) {
|
|
3916
|
+
if (!item) return "";
|
|
3917
|
+
if (item.key === "open") {
|
|
3918
|
+
const result = await ufanetOpenSmart({ state: agentState });
|
|
3919
|
+
return formatUfanetSmartOpenResult(result);
|
|
3920
|
+
}
|
|
3921
|
+
if (item.key === "intercoms") {
|
|
3922
|
+
const rows = await ufanetGetIntercoms();
|
|
3923
|
+
if (!rows.length) return "Доступные домофоны Уфанет не найдены.";
|
|
3924
|
+
return ["Домофоны Уфанет:", ...rows.map((row, index) => `${index + 1}. ${row.address || row.name || `Домофон #${row.id}`} — ID ${row.id}`)].join("\n");
|
|
3925
|
+
}
|
|
3926
|
+
if (item.key === "history") {
|
|
3927
|
+
const rows = await ufanetGetCallHistory({ page: 1, pageSize: 10 });
|
|
3928
|
+
if (!rows.results?.length) return "В истории Уфанет звонков не найдено.";
|
|
3929
|
+
return formatUfanetCallHistory(rows.results);
|
|
3930
|
+
}
|
|
3931
|
+
if (item.key === "notifications-on") {
|
|
3932
|
+
await setUfanetNotifications(true);
|
|
3933
|
+
return "Уведомления Уфанет включены. При новом вызове в CLI можно ответить `да` или `нет`.";
|
|
3934
|
+
}
|
|
3935
|
+
if (item.key === "notifications-off") {
|
|
3936
|
+
await setUfanetNotifications(false);
|
|
3937
|
+
return "Уведомления Уфанет выключены.";
|
|
3938
|
+
}
|
|
3939
|
+
if (item.key === "notifications-status") {
|
|
3940
|
+
const status = await getUfanetStatus();
|
|
3941
|
+
return `Уведомления Уфанет: ${status.notifications}, интервал ${status.intervalSeconds} сек.`;
|
|
3942
|
+
}
|
|
3943
|
+
if (item.key === "cameras") {
|
|
3944
|
+
const rows = await ufanetGetCameras();
|
|
3945
|
+
return formatUfanetCameraList(rows);
|
|
3946
|
+
}
|
|
3947
|
+
if (item.key === "status") {
|
|
3948
|
+
const status = await getUfanetStatus();
|
|
3949
|
+
return `Уфанет: ${status.configured ? "настроен" : "не настроен"}, ${status.enabled ? "включен" : "выключен"}, уведомления ${status.notifications}.`;
|
|
3950
|
+
}
|
|
3951
|
+
if (item.key === "delete") {
|
|
3952
|
+
return "Удаление подключения выполняется явной командой: `/ufanet delete`.";
|
|
3953
|
+
}
|
|
3954
|
+
return "";
|
|
3829
3955
|
}
|
|
3830
3956
|
|
|
3831
3957
|
async function setupUfanetConnector() {
|
|
@@ -4013,7 +4139,7 @@ async function watchUfanetCalls(options = {}) {
|
|
|
4013
4139
|
await updateUfanetLastSeen(lastSeen);
|
|
4014
4140
|
}
|
|
4015
4141
|
if (options.once) {
|
|
4016
|
-
printTable(initialRows, [["
|
|
4142
|
+
printTable(formatUfanetCallRows(initialRows), [["index", "#"], ["calledAt", "Когда"], ["address", "Адрес"], ["porch", "Подъезд"]]);
|
|
4017
4143
|
return;
|
|
4018
4144
|
}
|
|
4019
4145
|
console.log(`Уфанет: слежу за новыми вызовами каждые ${intervalSeconds} сек. Остановить: Ctrl+C.`);
|
|
@@ -4035,11 +4161,9 @@ async function watchUfanetCalls(options = {}) {
|
|
|
4035
4161
|
console.log("");
|
|
4036
4162
|
console.log("Новый вызов домофона Уфанет:");
|
|
4037
4163
|
printKeyValue({
|
|
4038
|
-
|
|
4039
|
-
calledAt: row.calledAt || "-",
|
|
4164
|
+
calledAt: formatUfanetCallDate(row.calledAt),
|
|
4040
4165
|
address: row.address || "-",
|
|
4041
4166
|
porch: row.porch || "-",
|
|
4042
|
-
flat: row.flat || "-",
|
|
4043
4167
|
});
|
|
4044
4168
|
}
|
|
4045
4169
|
lastSeen = ufanetCallKey(rows[0]) || lastSeen;
|
|
@@ -4066,6 +4190,8 @@ async function executeUfanetTool(tool, args = {}) {
|
|
|
4066
4190
|
if (tool === "ufanet_call_history") return ufanetGetCallHistory({ page: args.page || 1, pageSize: args.pageSize || args.page_size || args.limit || 10 });
|
|
4067
4191
|
if (tool === "ufanet_call_links") return ufanetGetCallLinks(args.uuid || args.id);
|
|
4068
4192
|
if (tool === "ufanet_cameras") return ufanetGetCameras();
|
|
4193
|
+
if (tool === "ufanet_camera_open") return ufanetOpenCamera(args.number || args.id || args.camera || 1);
|
|
4194
|
+
if (tool === "ufanet_camera_snapshot") return ufanetCameraSnapshot(args.number || args.id || args.camera || 1);
|
|
4069
4195
|
throw new Error(`Ufanet tool неизвестен: ${tool}`);
|
|
4070
4196
|
}
|
|
4071
4197
|
|
|
@@ -4249,6 +4375,126 @@ async function ufanetGetCameras() {
|
|
|
4249
4375
|
}));
|
|
4250
4376
|
}
|
|
4251
4377
|
|
|
4378
|
+
function formatUfanetCameraList(rows = []) {
|
|
4379
|
+
if (!rows.length) return "Камеры Уфанет не найдены.";
|
|
4380
|
+
const lines = [
|
|
4381
|
+
"Камеры Уфанет:",
|
|
4382
|
+
...rows.slice(0, 20).map((row, index) => {
|
|
4383
|
+
const title = formatUfanetCameraTitle(row);
|
|
4384
|
+
const video = row.rtspUrl ? "видео доступно" : "видео недоступно";
|
|
4385
|
+
return `${index + 1}. ${title} — ${row.address || "-"} (${video})`;
|
|
4386
|
+
}),
|
|
4387
|
+
"",
|
|
4388
|
+
"Что можно сделать:",
|
|
4389
|
+
"- открыть видео: /ufanet camera open НОМЕР",
|
|
4390
|
+
"- сохранить снимок: /ufanet camera snapshot НОМЕР",
|
|
4391
|
+
"",
|
|
4392
|
+
"RTSP-ссылки с токенами не выводятся в консоль. Для просмотра нужен установленный видеоплеер, например VLC; для снимка нужен ffmpeg.",
|
|
4393
|
+
];
|
|
4394
|
+
return lines.join("\n");
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
function formatUfanetCameraTitle(row = {}) {
|
|
4398
|
+
const title = String(row.title || "").trim().replace(/^\d+\s*,\s*/u, "");
|
|
4399
|
+
return title || "Камера";
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
function formatUfanetCallRows(rows = []) {
|
|
4403
|
+
return rows.map((row, index) => ({
|
|
4404
|
+
index: index + 1,
|
|
4405
|
+
calledAt: formatUfanetCallDate(row.calledAt),
|
|
4406
|
+
address: row.address || "-",
|
|
4407
|
+
porch: row.porch || "-",
|
|
4408
|
+
}));
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
function formatUfanetCallHistory(rows = []) {
|
|
4412
|
+
if (!rows.length) return "В истории Уфанет звонков не найдено.";
|
|
4413
|
+
return [
|
|
4414
|
+
"История звонков Уфанет:",
|
|
4415
|
+
...formatUfanetCallRows(rows).map((row) => `${row.index}. ${row.calledAt} — ${row.address}${row.porch && row.porch !== "-" ? `, подъезд ${row.porch}` : ""}`),
|
|
4416
|
+
"",
|
|
4417
|
+
"Служебные UUID скрыты. Если нужна запись конкретного звонка, запросите ее по номеру из истории.",
|
|
4418
|
+
].join("\n");
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
function formatUfanetCallDate(value) {
|
|
4422
|
+
if (!value) return "-";
|
|
4423
|
+
const date = new Date(value);
|
|
4424
|
+
if (!Number.isNaN(date.getTime())) {
|
|
4425
|
+
return new Intl.DateTimeFormat("ru-RU", { dateStyle: "short", timeStyle: "short" }).format(date);
|
|
4426
|
+
}
|
|
4427
|
+
return String(value);
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
function safeUfanetCamera(row = {}) {
|
|
4431
|
+
return {
|
|
4432
|
+
provider: "ufanet",
|
|
4433
|
+
title: row.title || "",
|
|
4434
|
+
address: row.address || "",
|
|
4435
|
+
type: row.type || "",
|
|
4436
|
+
hasVideo: Boolean(row.rtspUrl),
|
|
4437
|
+
};
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
function resolveUfanetCamera(rows = [], selector = 1) {
|
|
4441
|
+
if (!rows.length) throw new Error("Камеры Уфанет не найдены.");
|
|
4442
|
+
const raw = String(selector || "1").trim();
|
|
4443
|
+
const index = Number(raw);
|
|
4444
|
+
if (Number.isInteger(index) && index >= 1 && index <= rows.length) return rows[index - 1];
|
|
4445
|
+
const normalized = normalizeGeoText(raw);
|
|
4446
|
+
const match = rows.find((row) => String(row.number || "") === raw)
|
|
4447
|
+
|| rows.find((row) => normalizeGeoText(`${row.title || ""} ${row.address || ""}`).includes(normalized));
|
|
4448
|
+
if (match) return match;
|
|
4449
|
+
throw new Error(`Не нашел камеру: ${raw}. Сначала посмотрите список: /ufanet cameras`);
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
async function ufanetOpenCamera(selector = 1) {
|
|
4453
|
+
const rows = await ufanetGetCameras();
|
|
4454
|
+
const camera = resolveUfanetCamera(rows, selector);
|
|
4455
|
+
if (!camera.rtspUrl) throw new Error("У этой камеры нет доступного видеопотока.");
|
|
4456
|
+
await openUrl(camera.rtspUrl);
|
|
4457
|
+
return {
|
|
4458
|
+
...safeUfanetCamera(camera),
|
|
4459
|
+
status: "camera-opened",
|
|
4460
|
+
message: "Открываю видеопоток во внешнем плеере. Если окно не появилось, установите VLC и назначьте его для RTSP-ссылок.",
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
async function ufanetCameraSnapshot(selector = 1) {
|
|
4465
|
+
const rows = await ufanetGetCameras();
|
|
4466
|
+
const camera = resolveUfanetCamera(rows, selector);
|
|
4467
|
+
if (!camera.rtspUrl) throw new Error("У этой камеры нет доступного видеопотока для снимка.");
|
|
4468
|
+
const outputDir = path.join(CONFIG_DIR, "artifacts", "ufanet");
|
|
4469
|
+
await mkdir(outputDir, { recursive: true });
|
|
4470
|
+
const fileName = `${slugForFile(formatUfanetCameraTitle(camera))}-${timestampForFile()}.jpg`;
|
|
4471
|
+
const outputFile = path.join(outputDir, fileName);
|
|
4472
|
+
try {
|
|
4473
|
+
await runCommand("ffmpeg", [
|
|
4474
|
+
"-y",
|
|
4475
|
+
"-rtsp_transport",
|
|
4476
|
+
"tcp",
|
|
4477
|
+
"-i",
|
|
4478
|
+
camera.rtspUrl,
|
|
4479
|
+
"-frames:v",
|
|
4480
|
+
"1",
|
|
4481
|
+
outputFile,
|
|
4482
|
+
], { timeoutMs: 20000 });
|
|
4483
|
+
} catch (error) {
|
|
4484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4485
|
+
if (/ENOENT|not recognized|не является|cannot find|spawn/i.test(message)) {
|
|
4486
|
+
throw new Error("Для снимков с камеры нужен ffmpeg. Установите ffmpeg и повторите: /ufanet camera snapshot НОМЕР");
|
|
4487
|
+
}
|
|
4488
|
+
throw new Error(`Не смог сохранить снимок с камеры: ${message}`);
|
|
4489
|
+
}
|
|
4490
|
+
saveArtifact("ufanet-camera-snapshot", formatUfanetCameraTitle(camera), outputFile, safeUfanetCamera(camera));
|
|
4491
|
+
return {
|
|
4492
|
+
...safeUfanetCamera(camera),
|
|
4493
|
+
status: "camera-snapshot-saved",
|
|
4494
|
+
file: outputFile,
|
|
4495
|
+
};
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4252
4498
|
function printYandexServices(options = {}) {
|
|
4253
4499
|
const rows = Object.entries(YANDEX_CONNECTOR_SERVICES)
|
|
4254
4500
|
.filter(([, service]) => !options.status || service.status === options.status)
|
|
@@ -14799,12 +15045,19 @@ async function buildUfanetDirectAnswer(question, context = {}) {
|
|
|
14799
15045
|
if (/(истори|звонк|кто звонил|последн)/iu.test(normalized)) {
|
|
14800
15046
|
const rows = await ufanetGetCallHistory({ page: 1, pageSize: 10 });
|
|
14801
15047
|
if (!rows.results?.length) return "В истории Уфанет звонков не найдено.";
|
|
14802
|
-
return
|
|
15048
|
+
return formatUfanetCallHistory(rows.results);
|
|
15049
|
+
}
|
|
15050
|
+
if (/(сним|скриншот|фото|кадр)/iu.test(normalized) && /(камер|видео|домофон)/iu.test(normalized)) {
|
|
15051
|
+
const result = await ufanetCameraSnapshot(extractUfanetCameraSelector(question) || 1);
|
|
15052
|
+
return `Снимок с камеры сохранен: ${result.file}`;
|
|
15053
|
+
}
|
|
15054
|
+
if (/(открой|покажи|запусти|включи)/iu.test(normalized) && /(камер|видео)/iu.test(normalized)) {
|
|
15055
|
+
const result = await ufanetOpenCamera(extractUfanetCameraSelector(question) || 1);
|
|
15056
|
+
return result.message;
|
|
14803
15057
|
}
|
|
14804
15058
|
if (/(камер|rtsp|видео)/iu.test(normalized)) {
|
|
14805
15059
|
const rows = await ufanetGetCameras();
|
|
14806
|
-
|
|
14807
|
-
return ["Камеры Уфанет:", ...rows.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || row.number || "камера"} — ${row.address || "-"}${row.rtspUrl ? `, ${row.rtspUrl}` : ""}`)].join("\n");
|
|
15060
|
+
return formatUfanetCameraList(rows);
|
|
14808
15061
|
}
|
|
14809
15062
|
const intercoms = await ufanetGetIntercoms();
|
|
14810
15063
|
if (!intercoms.length) return "Доступные домофоны Уфанет не найдены.";
|
|
@@ -14816,6 +15069,12 @@ function extractSecondsFromText(text) {
|
|
|
14816
15069
|
return match ? Number(match[1]) : 0;
|
|
14817
15070
|
}
|
|
14818
15071
|
|
|
15072
|
+
function extractUfanetCameraSelector(text) {
|
|
15073
|
+
return String(text || "").match(/(?:камер[ауые]?|номер|№|#)\s*(\d{1,4})/iu)?.[1]
|
|
15074
|
+
|| String(text || "").match(/\b(\d{1,4})\b/u)?.[1]
|
|
15075
|
+
|| "";
|
|
15076
|
+
}
|
|
15077
|
+
|
|
14819
15078
|
function detectDirectDataFields(normalizedQuestion) {
|
|
14820
15079
|
const fields = [];
|
|
14821
15080
|
if (/(директ|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
|
|
@@ -15328,8 +15587,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
15328
15587
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
15329
15588
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
15330
15589
|
"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 {}.",
|
|
15331
|
-
"Ufanet tools: ufanet_status {}, ufanet_intercoms {}, ufanet_open_intercom {id,confirm}, ufanet_call_history {page,limit}, ufanet_call_links {uuid}, ufanet_cameras {}.",
|
|
15332
|
-
"Опасные 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.",
|
|
15590
|
+
"Ufanet tools: ufanet_status {}, ufanet_intercoms {}, ufanet_open_intercom {id,confirm}, ufanet_call_history {page,limit}, ufanet_call_links {uuid}, ufanet_cameras {}, ufanet_camera_open {number}, ufanet_camera_snapshot {number}.",
|
|
15591
|
+
"Опасные 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, ufanet_camera_open, ufanet_camera_snapshot.",
|
|
15333
15592
|
"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.",
|
|
15334
15593
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
15335
15594
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
@@ -15439,12 +15698,18 @@ function inferToolPlan(question, options = {}) {
|
|
|
15439
15698
|
if (/(истори|звонк|кто\s+звонил|последн)/iu.test(normalized) && !/(ссылк|запис|видео)/iu.test(normalized)) {
|
|
15440
15699
|
return { steps: [{ tool: "ufanet_call_history", args: { limit: 10 } }] };
|
|
15441
15700
|
}
|
|
15442
|
-
if (/(
|
|
15701
|
+
if (/(сним|скриншот|фото|кадр)/iu.test(normalized) && /(камер|видео|домофон)/iu.test(normalized)) {
|
|
15702
|
+
return { steps: [{ tool: "ufanet_camera_snapshot", args: { number: extractUfanetCameraSelector(question) || 1 } }] };
|
|
15703
|
+
}
|
|
15704
|
+
if (/(открой|покажи|запусти|включи)/iu.test(normalized) && /(камер|видео)/iu.test(normalized)) {
|
|
15705
|
+
return { steps: [{ tool: "ufanet_camera_open", args: { number: extractUfanetCameraSelector(question) || 1 } }] };
|
|
15706
|
+
}
|
|
15707
|
+
if (/(камер|rtsp|видео)/iu.test(normalized)) return { steps: [{ tool: "ufanet_cameras", args: {} }] };
|
|
15708
|
+
if (/(ссылк|запис|preview|превью)/iu.test(normalized)) {
|
|
15443
15709
|
const uuid = extractUuid(question);
|
|
15444
15710
|
if (!uuid) return { directAnswer: "Для ссылки на запись нужен UUID звонка. Сначала посмотрите историю: /ufanet history." };
|
|
15445
15711
|
return { steps: [{ tool: "ufanet_call_links", args: { uuid } }] };
|
|
15446
15712
|
}
|
|
15447
|
-
if (/(камер|rtsp|видео)/iu.test(normalized)) return { steps: [{ tool: "ufanet_cameras", args: {} }] };
|
|
15448
15713
|
if (/(статус|подключ|аккаунт|договор)/iu.test(normalized)) return { steps: [{ tool: "ufanet_status", args: {} }] };
|
|
15449
15714
|
return { steps: [{ tool: "ufanet_intercoms", args: {} }] };
|
|
15450
15715
|
}
|
|
@@ -16103,9 +16368,11 @@ function formatToolResult(result, options) {
|
|
|
16103
16368
|
if (row.type === "empty") return "Уфанет подключен, но доступных домофонов не найдено.";
|
|
16104
16369
|
if (row.type === "opened" && row.result) return formatUfanetOpenResult(row.result, row.choice || {});
|
|
16105
16370
|
if (row.provider === "ufanet" && (row.status === "opened" || row.status === "not-opened")) return `Уфанет: домофон #${row.id} ${row.status === "opened" ? "открыт" : "не открылся"}.`;
|
|
16371
|
+
if (row.provider === "ufanet" && row.status === "camera-opened") return row.message || `Открываю камеру Уфанет: ${row.title || row.number || row.address || "-"}.`;
|
|
16372
|
+
if (row.provider === "ufanet" && row.status === "camera-snapshot-saved") return `Снимок с камеры сохранен: ${row.file}`;
|
|
16106
16373
|
if (row.provider === "ufanet" && row.uuid && (row.url || row.preview)) return `Уфанет: запись звонка ${row.uuid}\nСсылка: ${row.url || "-"}\nПревью: ${row.preview || "-"}`;
|
|
16107
|
-
if (row.rtspUrl) return `Камера Уфанет ${row.title || row.number}: ${row.address || "-"}
|
|
16108
|
-
if (row.calledAt && row.uuid) return `Звонок Уфанет: ${row.calledAt}, ${row.address || "-"}, подъезд ${row.porch || "-"}
|
|
16374
|
+
if (row.rtspUrl || row.hasVideo !== undefined) return `Камера Уфанет ${row.title || row.number || "-"}: ${row.address || "-"}${row.rtspUrl || row.hasVideo ? "\nВидео доступно. Открыть: /ufanet camera open НОМЕР. Снимок: /ufanet camera snapshot НОМЕР." : ""}`;
|
|
16375
|
+
if (row.calledAt && row.uuid) return `Звонок Уфанет: ${formatUfanetCallDate(row.calledAt)}, ${row.address || "-"}, подъезд ${row.porch || "-"}`;
|
|
16109
16376
|
if (row.id && (row.address || row.role || row.blocked !== undefined)) return `Домофон Уфанет #${row.id}: ${row.name || row.address || "-"}${row.blocked === "yes" ? " (заблокирован)" : ""}`;
|
|
16110
16377
|
if (row.configured !== undefined && row.enabled !== undefined && row.contract !== undefined) return `Уфанет: ${row.configured ? "настроен" : "не настроен"}, ${row.enabled ? "включен" : "выключен"}${row.contract ? `, договор ${row.contract}` : ""}.`;
|
|
16111
16378
|
if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
|
|
@@ -20248,6 +20515,7 @@ function runCommand(command, args, options = {}) {
|
|
|
20248
20515
|
...(options.env || {}),
|
|
20249
20516
|
},
|
|
20250
20517
|
}, (error, stdout, stderr) => {
|
|
20518
|
+
if (timer) clearTimeout(timer);
|
|
20251
20519
|
if (error) {
|
|
20252
20520
|
if (process.platform === "win32" && (error.code === "ENOENT" || error.code === "EINVAL") && !options.cmdFallback) {
|
|
20253
20521
|
runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", quoteWindowsCommand(command, args)], {
|
|
@@ -20263,6 +20531,10 @@ function runCommand(command, args, options = {}) {
|
|
|
20263
20531
|
|
|
20264
20532
|
resolve({ stdout, stderr });
|
|
20265
20533
|
});
|
|
20534
|
+
const timer = options.timeoutMs ? setTimeout(() => {
|
|
20535
|
+
child.kill();
|
|
20536
|
+
reject(new Error(`Command timeout after ${options.timeoutMs} ms: ${command}`));
|
|
20537
|
+
}, Number(options.timeoutMs)) : null;
|
|
20266
20538
|
|
|
20267
20539
|
if (options.inherit) {
|
|
20268
20540
|
child.stdout?.pipe(process.stdout);
|
|
@@ -101,8 +101,10 @@ Toolset `ufanet` включает tools для личного домофона
|
|
|
101
101
|
- `ufanet_open_intercom` - открыть домофон по ID;
|
|
102
102
|
- `ufanet_call_history` - история звонков;
|
|
103
103
|
- `ufanet_call_links` - ссылка на запись звонка по UUID;
|
|
104
|
-
- `ufanet_cameras` -
|
|
104
|
+
- `ufanet_cameras` - безопасный список камер без вывода RTSP-токенов;
|
|
105
|
+
- `ufanet_camera_open` - открыть видеопоток камеры во внешнем плеере;
|
|
106
|
+
- `ufanet_camera_snapshot` - сохранить локальный снимок камеры через ffmpeg.
|
|
105
107
|
|
|
106
108
|
Уведомления о новых вызовах доступны командой `iola ufanet watch`; включение/выключение настройки - `iola ufanet notifications on|off`.
|
|
107
109
|
|
|
108
|
-
Открытие домофона требует явного подтверждения `confirm=true`. Договор,
|
|
110
|
+
Открытие домофона требует явного подтверждения `confirm=true`. Договор, пароль, JWT и RTSP-ссылки с токенами хранятся локально и не выводятся в ответах.
|
|
@@ -91,6 +91,7 @@ iola yandex token delete
|
|
|
91
91
|
Мой домофон:
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
+
iola ufanet
|
|
94
95
|
iola ufanet setup
|
|
95
96
|
iola ufanet status
|
|
96
97
|
iola ufanet intercoms
|
|
@@ -99,6 +100,8 @@ iola ufanet open ID
|
|
|
99
100
|
iola ufanet history --limit 10
|
|
100
101
|
iola ufanet links UUID
|
|
101
102
|
iola ufanet cameras
|
|
103
|
+
iola ufanet camera open НОМЕР
|
|
104
|
+
iola ufanet camera snapshot НОМЕР
|
|
102
105
|
iola ufanet watch --seconds 10
|
|
103
106
|
iola ufanet notifications on
|
|
104
107
|
iola ufanet notifications off
|
package/wiki//320/234/320/276/320/271-/320/264/320/276/320/274/320/276/321/204/320/276/320/275.md
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
Рабочий провайдер:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
+
iola ufanet
|
|
10
11
|
iola ufanet setup
|
|
11
12
|
iola ufanet status
|
|
12
13
|
iola ufanet intercoms
|
|
13
14
|
iola ufanet history
|
|
14
15
|
iola ufanet cameras
|
|
16
|
+
iola ufanet camera open НОМЕР
|
|
17
|
+
iola ufanet camera snapshot НОМЕР
|
|
15
18
|
iola ufanet links UUID
|
|
16
19
|
iola ufanet open
|
|
17
20
|
iola ufanet open ID
|
|
@@ -22,6 +25,8 @@ iola ufanet notifications status
|
|
|
22
25
|
iola ufanet delete
|
|
23
26
|
```
|
|
24
27
|
|
|
28
|
+
В интерактивном CLI команда `/ufanet` открывает отдельное меню домофона. В нем можно выбрать действие цифрой: открыть домофон, посмотреть список, историю, уведомления, камеры или статус.
|
|
29
|
+
|
|
25
30
|
При настройке нужны:
|
|
26
31
|
|
|
27
32
|
- номер договора Уфанет;
|
|
@@ -48,12 +53,33 @@ UFANET_PASSWORD
|
|
|
48
53
|
- открыть домофон по ID;
|
|
49
54
|
- показать историю звонков;
|
|
50
55
|
- получить ссылку на запись звонка по UUID;
|
|
51
|
-
- показать
|
|
52
|
-
-
|
|
56
|
+
- показать камеры без вывода технических RTSP-токенов;
|
|
57
|
+
- открыть видеопоток камеры во внешнем плеере;
|
|
58
|
+
- сохранить локальный снимок с камеры;
|
|
53
59
|
- получать уведомления о новых вызовах, пока CLI открыт;
|
|
54
60
|
- включить или выключить настройку уведомлений;
|
|
55
61
|
- удалить локальное подключение.
|
|
56
62
|
|
|
63
|
+
Камеры показываются как обычный список: номер, название, адрес и признак доступности видео. Сами RTSP-ссылки с токенами в консоль не выводятся.
|
|
64
|
+
|
|
65
|
+
Открыть видео:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
iola ufanet camera open 1
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
CLI передает поток во внешний видеоплеер. На Windows удобнее всего поставить VLC и назначить его для RTSP-ссылок.
|
|
72
|
+
|
|
73
|
+
Сохранить один кадр в локальный файл:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
iola ufanet camera snapshot 1
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Для снимка нужен `ffmpeg`. Файл сохраняется в `~/.iola/artifacts/ufanet/` и регистрируется в artifacts CLI.
|
|
80
|
+
|
|
81
|
+
История звонков в обычном выводе показывает время, адрес и подъезд. Служебные UUID скрываются, чтобы не перегружать пользователя техническими идентификаторами.
|
|
82
|
+
|
|
57
83
|
Открытие двери всегда требует явного действия пользователя. Если домофон один, фраза `открой домофон` считается таким действием. Если домофонов несколько, CLI выводит адреса с цифрами, а выбранная цифра считается подтверждением.
|
|
58
84
|
|
|
59
85
|
Уведомления сейчас работают через периодическую проверку истории звонков Уфанета:
|
|
@@ -416,7 +416,9 @@ iola yandex go open --from "Йошкар-Ола, Красноармейская
|
|
|
416
416
|
- открыть выбранный домофон по ID;
|
|
417
417
|
- показать историю звонков;
|
|
418
418
|
- получить ссылку на запись звонка по UUID;
|
|
419
|
-
- показать камеры
|
|
419
|
+
- показать камеры без вывода RTSP-токенов;
|
|
420
|
+
- открыть видео камеры во внешнем плеере;
|
|
421
|
+
- сохранить локальный снимок камеры;
|
|
420
422
|
- удалить локальное подключение.
|
|
421
423
|
|
|
422
424
|
Открытие двери всегда требует явного подтверждения пользователя.
|
|
@@ -429,6 +431,8 @@ iola ufanet intercoms
|
|
|
429
431
|
iola ufanet open ID
|
|
430
432
|
iola ufanet history
|
|
431
433
|
iola ufanet cameras
|
|
434
|
+
iola ufanet camera open 1
|
|
435
|
+
iola ufanet camera snapshot 1
|
|
432
436
|
iola ufanet watch
|
|
433
437
|
iola ufanet notifications on
|
|
434
438
|
iola ufanet notifications off
|