@iola_adm/iola-cli 0.2.47 → 0.2.49
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 +3 -1
- package/package.json +1 -1
- package/skills/ufanet-intercom/SKILL.md +4 -2
- package/src/cli.js +392 -35
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +2 -0
- package/wiki//320/234/320/276/320/271-/320/264/320/276/320/274/320/276/321/204/320/276/320/275.md +8 -2
package/README.md
CHANGED
|
@@ -210,8 +210,10 @@ Yandex tools уже доступны: профиль Yandex ID, расширен
|
|
|
210
210
|
Мой домофон:
|
|
211
211
|
|
|
212
212
|
```bash
|
|
213
|
+
iola ufanet
|
|
213
214
|
iola ufanet setup
|
|
214
215
|
iola ufanet intercoms
|
|
216
|
+
iola ufanet open
|
|
215
217
|
iola ufanet open ID
|
|
216
218
|
iola ufanet history
|
|
217
219
|
iola ufanet cameras
|
|
@@ -222,7 +224,7 @@ iola dom_ru
|
|
|
222
224
|
iola rostelecom
|
|
223
225
|
```
|
|
224
226
|
|
|
225
|
-
Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков, камеры/RTSP и уведомления о новых вызовах через опрос истории звонков, пока CLI открыт. Дом.ру и Ростелеком добавлены как видимые направления `в разработке`.
|
|
227
|
+
`/ufanet` в интерактивном CLI открывает отдельное меню домофона с выбором действия цифрой. Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков, камеры/RTSP и уведомления о новых вызовах через опрос истории звонков, пока CLI открыт. Команда `открой домофон` открывает единственный домофон сразу; если домофонов несколько, CLI просит выбрать адрес цифрой. При уведомлении о вызове можно ответить `да`, `да открой` или `нет`. Дом.ру и Ростелеком добавлены как видимые направления `в разработке`.
|
|
226
228
|
|
|
227
229
|
Инструкция: [Мой домофон](https://github.com/adm-iola/iola-cli/wiki/Мой-домофон).
|
|
228
230
|
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ description: Мой домофон Уфанет: список домофонов
|
|
|
11
11
|
|
|
12
12
|
- `ufanet_status` - проверить, подключен ли Уфанет.
|
|
13
13
|
- `ufanet_intercoms` - показать доступные домофоны пользователя.
|
|
14
|
-
- `ufanet_open_intercom` - открыть домофон по ID
|
|
14
|
+
- `ufanet_open_intercom` - открыть домофон по ID; если ID не указан, CLI сам получает список домофонов.
|
|
15
15
|
- `ufanet_call_history` - показать историю звонков домофона.
|
|
16
16
|
- `ufanet_call_links` - получить ссылку на запись/превью звонка по UUID.
|
|
17
17
|
- `ufanet_cameras` - показать доступные камеры и RTSP-ссылки.
|
|
@@ -22,6 +22,7 @@ description: Мой домофон Уфанет: список домофонов
|
|
|
22
22
|
- `iola ufanet status` - проверить подключение.
|
|
23
23
|
- `iola ufanet intercoms` - список домофонов.
|
|
24
24
|
- `iola ufanet open ID` - открыть домофон после подтверждения.
|
|
25
|
+
- `iola ufanet open` - если домофон один, открыть его; если несколько, попросить выбрать цифрой.
|
|
25
26
|
- `iola ufanet history` - история звонков.
|
|
26
27
|
- `iola ufanet watch` - показывать новые вызовы, пока CLI открыт.
|
|
27
28
|
- `iola ufanet notifications on|off|status` - включить, выключить или проверить настройку уведомлений.
|
|
@@ -32,7 +33,8 @@ description: Мой домофон Уфанет: список домофонов
|
|
|
32
33
|
Безопасность:
|
|
33
34
|
|
|
34
35
|
- Для `ufanet_open_intercom` всегда нужен явный запрос пользователя и `confirm=true`.
|
|
35
|
-
- Если пользователь просит "открой домофон",
|
|
36
|
+
- Если пользователь просит "открой домофон", а ID не указан, CLI должен сам получить список домофонов. Если домофон один - открыть его. Если домофонов несколько - показать адреса с цифрами и ждать выбор цифрой; выбранная цифра считается подтверждением.
|
|
37
|
+
- Если включены уведомления и пришел вызов, в сообщении должен быть вопрос "Открыть?". Ответы `да`, `да открой`, `открой`, `ок` открывают сопоставленный домофон. Ответы `нет`, `отмена` отменяют.
|
|
36
38
|
- Не открывай домофон по косвенному намерению вроде "кто там", "звонят", "посмотри".
|
|
37
39
|
- Не выводи договор, пароль, JWT-токен и другие секреты.
|
|
38
40
|
- Если Уфанет не подключен, скажи запустить `/master` и выбрать `Мой домофон Уфанет`, либо команду `iola ufanet setup`.
|
package/src/cli.js
CHANGED
|
@@ -1187,7 +1187,7 @@ async function startAgentReadline() {
|
|
|
1187
1187
|
}
|
|
1188
1188
|
|
|
1189
1189
|
async function startAgentRawInput() {
|
|
1190
|
-
const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, renderedLines: 0, rawMode: true, pendingOutput: "", aiStatus: null, statusBar: false, statusRows: 0 };
|
|
1190
|
+
const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, renderedLines: 0, rawMode: true, pendingOutput: "", pendingAction: null, aiStatus: null, statusBar: false, statusRows: 0 };
|
|
1191
1191
|
const wasRaw = input.isRaw;
|
|
1192
1192
|
activateRawInput(input);
|
|
1193
1193
|
setupAgentStatusBar(state);
|
|
@@ -1195,6 +1195,7 @@ async function startAgentRawInput() {
|
|
|
1195
1195
|
await refreshAgentAiStatus(state);
|
|
1196
1196
|
const render = () => renderAgentInput(state);
|
|
1197
1197
|
render();
|
|
1198
|
+
const stopUfanetNotifications = setupAgentUfanetNotifications(state, render);
|
|
1198
1199
|
|
|
1199
1200
|
try {
|
|
1200
1201
|
while (true) {
|
|
@@ -1271,6 +1272,7 @@ async function startAgentRawInput() {
|
|
|
1271
1272
|
}
|
|
1272
1273
|
}
|
|
1273
1274
|
} finally {
|
|
1275
|
+
stopUfanetNotifications();
|
|
1274
1276
|
clearAgentInputArea(state);
|
|
1275
1277
|
finishAgentTerminalLine(state);
|
|
1276
1278
|
if (!wasRaw) input.setRawMode(false);
|
|
@@ -1280,10 +1282,15 @@ async function startAgentRawInput() {
|
|
|
1280
1282
|
|
|
1281
1283
|
async function handleAgentLine(line, state) {
|
|
1282
1284
|
if (!line.startsWith("/")) {
|
|
1283
|
-
|
|
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
|
+
}
|
|
1288
|
+
const pendingAnswer = await handlePendingAgentAction(line, state);
|
|
1289
|
+
const answer = pendingAnswer || await aiAsk(state.rawMode ? [line, "--quiet"] : [line], { history: state.history, state });
|
|
1284
1290
|
state.history.push({ role: "user", content: line });
|
|
1285
1291
|
state.history.push({ role: "assistant", content: answer });
|
|
1286
1292
|
if (state.rawMode) state.pendingOutput = answer;
|
|
1293
|
+
else if (pendingAnswer) printAiAnswer(pendingAnswer);
|
|
1287
1294
|
return false;
|
|
1288
1295
|
}
|
|
1289
1296
|
|
|
@@ -1305,6 +1312,10 @@ async function handleAgentLine(line, state) {
|
|
|
1305
1312
|
}
|
|
1306
1313
|
|
|
1307
1314
|
if (command === "help") {
|
|
1315
|
+
if (state.pendingAction?.type === "ufanet_menu") {
|
|
1316
|
+
await printUfanetMenu({ agentState: state, pending: true });
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1308
1319
|
printAgentHelp();
|
|
1309
1320
|
return false;
|
|
1310
1321
|
}
|
|
@@ -1600,6 +1611,21 @@ async function handleAgentLine(line, state) {
|
|
|
1600
1611
|
return false;
|
|
1601
1612
|
}
|
|
1602
1613
|
|
|
1614
|
+
if (command === "ufanet") {
|
|
1615
|
+
await handleUfanet(args.length ? args : ["menu"], state);
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
if (command === "dom_ru" || command === "domru") {
|
|
1620
|
+
await handleDomRu(args);
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
if (command === "rostelecom") {
|
|
1625
|
+
await handleRostelecom(args);
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1603
1629
|
const mapped = {
|
|
1604
1630
|
health: ["health", args],
|
|
1605
1631
|
doctor: ["doctor", args],
|
|
@@ -1615,10 +1641,6 @@ async function handleAgentLine(line, state) {
|
|
|
1615
1641
|
files: ["files", args],
|
|
1616
1642
|
archive: ["archive", args],
|
|
1617
1643
|
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],
|
|
1622
1644
|
changes: ["changes", args],
|
|
1623
1645
|
index: ["index", args],
|
|
1624
1646
|
reports: ["reports", args],
|
|
@@ -1901,6 +1923,146 @@ function flushPendingAgentOutput(state) {
|
|
|
1901
1923
|
printAiAnswer(text);
|
|
1902
1924
|
}
|
|
1903
1925
|
|
|
1926
|
+
async function handlePendingAgentAction(line, state) {
|
|
1927
|
+
const action = state?.pendingAction;
|
|
1928
|
+
if (!action) return "";
|
|
1929
|
+
const text = String(line || "").trim();
|
|
1930
|
+
const normalized = text.toLocaleLowerCase("ru-RU");
|
|
1931
|
+
if (/^(нет|не|отмена|cancel|no|n)$/iu.test(normalized)) {
|
|
1932
|
+
state.pendingAction = null;
|
|
1933
|
+
return "Действие отменено.";
|
|
1934
|
+
}
|
|
1935
|
+
if (action.type === "ufanet_select_open") {
|
|
1936
|
+
const number = Number(text.match(/\d+/u)?.[0] || 0);
|
|
1937
|
+
if (!number || number < 1 || number > action.choices.length) {
|
|
1938
|
+
return formatUfanetChoicePrompt(action.choices);
|
|
1939
|
+
}
|
|
1940
|
+
const choice = action.choices[number - 1];
|
|
1941
|
+
state.pendingAction = null;
|
|
1942
|
+
const result = await ufanetOpenIntercom(choice.id, { confirm: true });
|
|
1943
|
+
return formatUfanetOpenResult(result, choice);
|
|
1944
|
+
}
|
|
1945
|
+
if (action.type === "ufanet_confirm_open") {
|
|
1946
|
+
if (!/^(да|д|yes|y|ok|ок|открой|да\s+открой|открывай|пусти)/iu.test(normalized)) {
|
|
1947
|
+
return "Ответьте `да`, чтобы открыть домофон, или `нет`, чтобы отменить.";
|
|
1948
|
+
}
|
|
1949
|
+
state.pendingAction = null;
|
|
1950
|
+
const result = await ufanetOpenIntercom(action.id, { confirm: true });
|
|
1951
|
+
return formatUfanetOpenResult(result, action);
|
|
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
|
+
}
|
|
1968
|
+
return "";
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function setupAgentUfanetNotifications(state, render) {
|
|
1972
|
+
if (!input.isTTY) return () => {};
|
|
1973
|
+
let stopped = false;
|
|
1974
|
+
let timer = null;
|
|
1975
|
+
let busy = false;
|
|
1976
|
+
const schedule = (seconds = 10) => {
|
|
1977
|
+
if (stopped) return;
|
|
1978
|
+
timer = setTimeout(loop, normalizeUfanetPollInterval(seconds) * 1000);
|
|
1979
|
+
};
|
|
1980
|
+
const loop = async () => {
|
|
1981
|
+
if (stopped || busy) {
|
|
1982
|
+
schedule(10);
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
busy = true;
|
|
1986
|
+
let nextInterval = 10;
|
|
1987
|
+
try {
|
|
1988
|
+
const config = await loadConfig();
|
|
1989
|
+
const notificationConfig = config.domophones?.providers?.ufanet?.notifications || {};
|
|
1990
|
+
nextInterval = normalizeUfanetPollInterval(notificationConfig.intervalSeconds || 10);
|
|
1991
|
+
if (notificationConfig.enabled) {
|
|
1992
|
+
const history = await ufanetGetCallHistory({ page: 1, pageSize: 10 });
|
|
1993
|
+
const rows = history.results || [];
|
|
1994
|
+
let lastSeen = notificationConfig.lastSeen || "";
|
|
1995
|
+
if (!lastSeen && rows[0]) {
|
|
1996
|
+
await updateUfanetLastSeen(ufanetCallKey(rows[0]));
|
|
1997
|
+
} else {
|
|
1998
|
+
const fresh = [];
|
|
1999
|
+
for (const row of rows) {
|
|
2000
|
+
const key = ufanetCallKey(row);
|
|
2001
|
+
if (!key || key === lastSeen) break;
|
|
2002
|
+
fresh.push(row);
|
|
2003
|
+
}
|
|
2004
|
+
if (fresh.length > 0) {
|
|
2005
|
+
for (const row of fresh.reverse()) {
|
|
2006
|
+
await announceUfanetCallInAgent(row, state, render);
|
|
2007
|
+
}
|
|
2008
|
+
await updateUfanetLastSeen(ufanetCallKey(rows[0]) || lastSeen);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
if (process.env.IOLA_DEBUG === "1") {
|
|
2014
|
+
printAgentAsyncMessage(state, render, `Уфанет: ошибка проверки вызовов: ${error instanceof Error ? error.message : String(error)}`);
|
|
2015
|
+
}
|
|
2016
|
+
} finally {
|
|
2017
|
+
busy = false;
|
|
2018
|
+
schedule(nextInterval);
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
schedule(2);
|
|
2022
|
+
return () => {
|
|
2023
|
+
stopped = true;
|
|
2024
|
+
if (timer) clearTimeout(timer);
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
async function announceUfanetCallInAgent(row, state, render) {
|
|
2029
|
+
const match = await resolveUfanetIntercomForCall(row).catch(() => null);
|
|
2030
|
+
if (match?.id) {
|
|
2031
|
+
state.pendingAction = {
|
|
2032
|
+
type: "ufanet_confirm_open",
|
|
2033
|
+
id: match.id,
|
|
2034
|
+
address: match.address || row.address || "",
|
|
2035
|
+
name: match.name || "",
|
|
2036
|
+
createdAt: new Date().toISOString(),
|
|
2037
|
+
};
|
|
2038
|
+
printAgentAsyncMessage(state, render, [
|
|
2039
|
+
"Новый вызов домофона Уфанет:",
|
|
2040
|
+
`Адрес: ${row.address || match.address || "-"}`,
|
|
2041
|
+
`Время: ${row.calledAt || "-"}`,
|
|
2042
|
+
"Открыть? Ответьте `да` или `нет`.",
|
|
2043
|
+
].join("\n"));
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const intercoms = await ufanetGetIntercoms().catch(() => []);
|
|
2047
|
+
const choices = intercoms.map((item) => ({ id: item.id, name: item.name, address: item.address }));
|
|
2048
|
+
state.pendingAction = choices.length
|
|
2049
|
+
? { type: "ufanet_select_open", choices, createdAt: new Date().toISOString() }
|
|
2050
|
+
: null;
|
|
2051
|
+
printAgentAsyncMessage(state, render, [
|
|
2052
|
+
"Новый вызов домофона Уфанет:",
|
|
2053
|
+
`Адрес: ${row.address || "-"}`,
|
|
2054
|
+
`Время: ${row.calledAt || "-"}`,
|
|
2055
|
+
choices.length ? "Открыть? Выберите домофон номером:" : "Не смог сопоставить вызов с домофоном.",
|
|
2056
|
+
choices.length ? formatUfanetChoicePrompt(choices) : "",
|
|
2057
|
+
].filter(Boolean).join("\n"));
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function printAgentAsyncMessage(state, render, text) {
|
|
2061
|
+
clearAgentInputArea(state);
|
|
2062
|
+
output.write(`${text}\n`);
|
|
2063
|
+
render();
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1904
2066
|
function colorSlashSelection(row) {
|
|
1905
2067
|
if (!output.isTTY || process.env.NO_COLOR === "1") return row;
|
|
1906
2068
|
return `\x1b[38;5;213m${row}\x1b[0m`;
|
|
@@ -3571,12 +3733,29 @@ async function handleRostelecom() {
|
|
|
3571
3733
|
console.log("Пока доступна заготовка пункта в городских сервисах. API/авторизация будут добавлены после исследования провайдера.");
|
|
3572
3734
|
}
|
|
3573
3735
|
|
|
3574
|
-
async function handleUfanet(args = []) {
|
|
3736
|
+
async function handleUfanet(args = [], agentState = null) {
|
|
3575
3737
|
const [action = process.stdin.isTTY ? "menu" : "status", target, ...rest] = args;
|
|
3576
3738
|
const options = parseOptions(rest);
|
|
3577
3739
|
|
|
3578
|
-
if (action === "menu" || action === "choose") {
|
|
3579
|
-
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);
|
|
3580
3759
|
return;
|
|
3581
3760
|
}
|
|
3582
3761
|
|
|
@@ -3598,7 +3777,11 @@ async function handleUfanet(args = []) {
|
|
|
3598
3777
|
|
|
3599
3778
|
if (action === "open") {
|
|
3600
3779
|
const intercomId = target || options.id || options.intercom;
|
|
3601
|
-
if (!intercomId)
|
|
3780
|
+
if (!intercomId) {
|
|
3781
|
+
const result = await ufanetOpenSmart({ state: agentState });
|
|
3782
|
+
console.log(formatUfanetSmartOpenResult(result));
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3602
3785
|
const ok = options.yes || options.confirm || await confirm(`Открыть домофон Уфанет #${intercomId}? [y/N] `);
|
|
3603
3786
|
if (!ok) {
|
|
3604
3787
|
console.log("Открытие отменено.");
|
|
@@ -3661,28 +3844,88 @@ async function handleUfanet(args = []) {
|
|
|
3661
3844
|
iola ufanet delete`);
|
|
3662
3845
|
}
|
|
3663
3846
|
|
|
3664
|
-
async function printUfanetMenu() {
|
|
3847
|
+
async function printUfanetMenu(options = {}) {
|
|
3848
|
+
if (options.agentState && options.pending) {
|
|
3849
|
+
options.agentState.pendingAction = buildUfanetMenuPendingAction();
|
|
3850
|
+
}
|
|
3851
|
+
console.log(await formatUfanetMenu());
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
async function formatUfanetMenu(options = {}) {
|
|
3665
3855
|
const status = await getUfanetStatus();
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
{
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
{
|
|
3683
|
-
{
|
|
3684
|
-
{
|
|
3685
|
-
|
|
3856
|
+
const lines = [
|
|
3857
|
+
"Мой домофон",
|
|
3858
|
+
`Уфанет: ${status.configured ? "готово" : "не настроено"}; уведомления: ${status.notifications}.`,
|
|
3859
|
+
"Дом.ру: в разработке. Ростелеком: в разработке.",
|
|
3860
|
+
"",
|
|
3861
|
+
"Выберите действие:",
|
|
3862
|
+
...getUfanetMenuItems().map((item) => `${item.number}. ${item.title}`),
|
|
3863
|
+
"0. Назад",
|
|
3864
|
+
"",
|
|
3865
|
+
"Введите цифру или команду вида `/ufanet 3`. Пока открыт этот раздел, `/help` показывает справку по домофону.",
|
|
3866
|
+
];
|
|
3867
|
+
return options.compact ? lines.slice(4).join("\n") : lines.join("\n");
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
function getUfanetMenuItems() {
|
|
3871
|
+
return [
|
|
3872
|
+
{ number: 1, key: "open", title: "Открыть домофон" },
|
|
3873
|
+
{ number: 2, key: "intercoms", title: "Показать мои домофоны" },
|
|
3874
|
+
{ number: 3, key: "history", title: "История звонков" },
|
|
3875
|
+
{ number: 4, key: "notifications-on", title: "Включить уведомления о вызовах" },
|
|
3876
|
+
{ number: 5, key: "notifications-off", title: "Выключить уведомления" },
|
|
3877
|
+
{ number: 6, key: "notifications-status", title: "Статус уведомлений" },
|
|
3878
|
+
{ number: 7, key: "cameras", title: "Камеры" },
|
|
3879
|
+
{ number: 8, key: "status", title: "Статус подключения" },
|
|
3880
|
+
{ number: 9, key: "delete", title: "Удалить подключение Уфанет" },
|
|
3881
|
+
];
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
function buildUfanetMenuPendingAction() {
|
|
3885
|
+
return { type: "ufanet_menu", items: getUfanetMenuItems(), createdAt: new Date().toISOString() };
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
async function executeUfanetMenuItem(item, agentState = null) {
|
|
3889
|
+
if (!item) return "";
|
|
3890
|
+
if (item.key === "open") {
|
|
3891
|
+
const result = await ufanetOpenSmart({ state: agentState });
|
|
3892
|
+
return formatUfanetSmartOpenResult(result);
|
|
3893
|
+
}
|
|
3894
|
+
if (item.key === "intercoms") {
|
|
3895
|
+
const rows = await ufanetGetIntercoms();
|
|
3896
|
+
if (!rows.length) return "Доступные домофоны Уфанет не найдены.";
|
|
3897
|
+
return ["Домофоны Уфанет:", ...rows.map((row, index) => `${index + 1}. ${row.address || row.name || `Домофон #${row.id}`} — ID ${row.id}`)].join("\n");
|
|
3898
|
+
}
|
|
3899
|
+
if (item.key === "history") {
|
|
3900
|
+
const rows = await ufanetGetCallHistory({ page: 1, pageSize: 10 });
|
|
3901
|
+
if (!rows.results?.length) return "В истории Уфанет звонков не найдено.";
|
|
3902
|
+
return ["История звонков Уфанет:", ...rows.results.map((row, index) => `${index + 1}. ${row.calledAt || "-"} — ${row.address || "-"}${row.uuid ? `, UUID ${row.uuid}` : ""}`)].join("\n");
|
|
3903
|
+
}
|
|
3904
|
+
if (item.key === "notifications-on") {
|
|
3905
|
+
await setUfanetNotifications(true);
|
|
3906
|
+
return "Уведомления Уфанет включены. При новом вызове в CLI можно ответить `да` или `нет`.";
|
|
3907
|
+
}
|
|
3908
|
+
if (item.key === "notifications-off") {
|
|
3909
|
+
await setUfanetNotifications(false);
|
|
3910
|
+
return "Уведомления Уфанет выключены.";
|
|
3911
|
+
}
|
|
3912
|
+
if (item.key === "notifications-status") {
|
|
3913
|
+
const status = await getUfanetStatus();
|
|
3914
|
+
return `Уведомления Уфанет: ${status.notifications}, интервал ${status.intervalSeconds} сек.`;
|
|
3915
|
+
}
|
|
3916
|
+
if (item.key === "cameras") {
|
|
3917
|
+
const rows = await ufanetGetCameras();
|
|
3918
|
+
if (!rows.length) return "Камеры Уфанет не найдены.";
|
|
3919
|
+
return ["Камеры Уфанет:", ...rows.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || row.number || "камера"} — ${row.address || "-"}${row.rtspUrl ? `, ${row.rtspUrl}` : ""}`)].join("\n");
|
|
3920
|
+
}
|
|
3921
|
+
if (item.key === "status") {
|
|
3922
|
+
const status = await getUfanetStatus();
|
|
3923
|
+
return `Уфанет: ${status.configured ? "настроен" : "не настроен"}, ${status.enabled ? "включен" : "выключен"}, уведомления ${status.notifications}.`;
|
|
3924
|
+
}
|
|
3925
|
+
if (item.key === "delete") {
|
|
3926
|
+
return "Удаление подключения выполняется явной командой: `/ufanet delete`.";
|
|
3927
|
+
}
|
|
3928
|
+
return "";
|
|
3686
3929
|
}
|
|
3687
3930
|
|
|
3688
3931
|
async function setupUfanetConnector() {
|
|
@@ -3917,7 +4160,9 @@ function ufanetCallKey(row = {}) {
|
|
|
3917
4160
|
async function executeUfanetTool(tool, args = {}) {
|
|
3918
4161
|
if (tool === "ufanet_status") return getUfanetStatus();
|
|
3919
4162
|
if (tool === "ufanet_intercoms") return ufanetGetIntercoms();
|
|
3920
|
-
if (tool === "ufanet_open_intercom") return
|
|
4163
|
+
if (tool === "ufanet_open_intercom") return args.id || args.intercomId || args.intercom_id
|
|
4164
|
+
? ufanetOpenIntercom(args.id || args.intercomId || args.intercom_id, args)
|
|
4165
|
+
: ufanetOpenSmart({ ...args, state: args.state });
|
|
3921
4166
|
if (tool === "ufanet_call_history") return ufanetGetCallHistory({ page: args.page || 1, pageSize: args.pageSize || args.page_size || args.limit || 10 });
|
|
3922
4167
|
if (tool === "ufanet_call_links") return ufanetGetCallLinks(args.uuid || args.id);
|
|
3923
4168
|
if (tool === "ufanet_cameras") return ufanetGetCameras();
|
|
@@ -4011,6 +4256,59 @@ async function ufanetOpenIntercom(intercomId, options = {}) {
|
|
|
4011
4256
|
return { provider: "ufanet", status: payload?.result ? "opened" : "not-opened", id: Number(intercomId), result: Boolean(payload?.result) };
|
|
4012
4257
|
}
|
|
4013
4258
|
|
|
4259
|
+
async function ufanetOpenSmart(options = {}) {
|
|
4260
|
+
const intercomId = options.id || options.intercomId || options.intercom_id;
|
|
4261
|
+
if (intercomId) {
|
|
4262
|
+
const result = await ufanetOpenIntercom(intercomId, { confirm: true });
|
|
4263
|
+
return { type: "opened", result, choice: { id: intercomId } };
|
|
4264
|
+
}
|
|
4265
|
+
const intercoms = await ufanetGetIntercoms();
|
|
4266
|
+
if (intercoms.length === 0) return { type: "empty" };
|
|
4267
|
+
const choices = intercoms.map((item) => ({ id: item.id, name: item.name, address: item.address }));
|
|
4268
|
+
if (intercoms.length === 1) {
|
|
4269
|
+
const result = await ufanetOpenIntercom(intercoms[0].id, { confirm: true });
|
|
4270
|
+
return { type: "opened", result, choice: choices[0] };
|
|
4271
|
+
}
|
|
4272
|
+
if (options.state) {
|
|
4273
|
+
options.state.pendingAction = { type: "ufanet_select_open", choices, createdAt: new Date().toISOString() };
|
|
4274
|
+
}
|
|
4275
|
+
return { type: "select", choices };
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
function formatUfanetSmartOpenResult(value) {
|
|
4279
|
+
if (value.type === "empty") return "Уфанет подключен, но доступных домофонов не найдено.";
|
|
4280
|
+
if (value.type === "select") return formatUfanetChoicePrompt(value.choices);
|
|
4281
|
+
if (value.type === "opened") return formatUfanetOpenResult(value.result, value.choice);
|
|
4282
|
+
return "Не удалось выполнить действие с домофоном.";
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
function formatUfanetChoicePrompt(choices = []) {
|
|
4286
|
+
return [
|
|
4287
|
+
"Найдено несколько домофонов. Какой открыть? Ответьте цифрой:",
|
|
4288
|
+
...choices.map((item, index) => `${index + 1}. ${item.address || item.name || `Домофон #${item.id}`} — ID ${item.id}`),
|
|
4289
|
+
].join("\n");
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
function formatUfanetOpenResult(result, choice = {}) {
|
|
4293
|
+
const label = choice.address || choice.name || `ID ${result.id}`;
|
|
4294
|
+
return result.result
|
|
4295
|
+
? `Домофон открыт: ${label}.`
|
|
4296
|
+
: `Уфанет не подтвердил открытие домофона: ${label}.`;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
async function resolveUfanetIntercomForCall(row = {}) {
|
|
4300
|
+
const address = normalizeGeoText(row.address || "");
|
|
4301
|
+
if (!address) return null;
|
|
4302
|
+
const intercoms = await ufanetGetIntercoms();
|
|
4303
|
+
const matches = intercoms.filter((item) => {
|
|
4304
|
+
const itemAddress = normalizeGeoText(item.address || item.name || "");
|
|
4305
|
+
return itemAddress && (itemAddress.includes(address) || address.includes(itemAddress));
|
|
4306
|
+
});
|
|
4307
|
+
if (matches.length === 1) return matches[0];
|
|
4308
|
+
if (matches.length > 1) return null;
|
|
4309
|
+
return intercoms.length === 1 ? intercoms[0] : null;
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4014
4312
|
async function ufanetGetCallHistory(options = {}) {
|
|
4015
4313
|
const page = Math.max(1, Number(options.page || 1));
|
|
4016
4314
|
const pageSize = Math.max(1, Math.min(100, Number(options.pageSize || 10)));
|
|
@@ -12915,6 +13213,7 @@ async function setupIolaLocal(args) {
|
|
|
12915
13213
|
|
|
12916
13214
|
async function aiAsk(args, context = {}) {
|
|
12917
13215
|
const options = parseOptions(args);
|
|
13216
|
+
if (context.state) options.state = context.state;
|
|
12918
13217
|
const question = options._.join(" ").trim();
|
|
12919
13218
|
|
|
12920
13219
|
if (!question) {
|
|
@@ -12964,6 +13263,20 @@ async function aiAsk(args, context = {}) {
|
|
|
12964
13263
|
if (!options.quiet) console.log(userSkillAnswer);
|
|
12965
13264
|
return userSkillAnswer;
|
|
12966
13265
|
}
|
|
13266
|
+
const ufanetAnswer = await buildUfanetDirectAnswer(question, context);
|
|
13267
|
+
if (ufanetAnswer) {
|
|
13268
|
+
if (historyEnabled) {
|
|
13269
|
+
recordAskHistory({ question, answer: ufanetAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
13270
|
+
appendSessionExchange(sessionId, question, ufanetAnswer, dataContext, "");
|
|
13271
|
+
}
|
|
13272
|
+
emitEvent(options, "answer", { length: ufanetAnswer.length, sessionId, direct: true, ufanet: true });
|
|
13273
|
+
if (options.output) {
|
|
13274
|
+
await assertPermission("writeFiles");
|
|
13275
|
+
await writeFile(options.output, ufanetAnswer, "utf8");
|
|
13276
|
+
}
|
|
13277
|
+
if (!options.quiet) console.log(ufanetAnswer);
|
|
13278
|
+
return ufanetAnswer;
|
|
13279
|
+
}
|
|
12967
13280
|
if (/(контакт|адресн)/iu.test(question) && !isExplicitYandexDiskPathDelete(question)) {
|
|
12968
13281
|
const yandexContactAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
12969
13282
|
if (yandexContactAnswer) {
|
|
@@ -14561,6 +14874,48 @@ function cleanupCloudSaveText(question) {
|
|
|
14561
14874
|
.trim();
|
|
14562
14875
|
}
|
|
14563
14876
|
|
|
14877
|
+
async function buildUfanetDirectAnswer(question, context = {}) {
|
|
14878
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
14879
|
+
if (!/(домофон|уфанет|ufanet|дверь|подъезд)/iu.test(normalized)) return "";
|
|
14880
|
+
if (/(дом\.?ру|dom\.?ru)/iu.test(normalized)) return "Мой домофон Дом.ру пока в разработке. Сейчас реализован Уфанет: /ufanet.";
|
|
14881
|
+
if (/(ростелеком|rostelecom)/iu.test(normalized)) return "Мой домофон Ростелеком пока в разработке. Сейчас реализован Уфанет: /ufanet.";
|
|
14882
|
+
if (/(уведом|оповещ|сообщ).{0,40}(включ|получ|on|вкл)/iu.test(normalized) || /(включ|получ).{0,40}(уведом|оповещ|сообщ).{0,40}(домофон)/iu.test(normalized)) {
|
|
14883
|
+
await setUfanetNotifications(true, { intervalSeconds: extractSecondsFromText(question) || 10 });
|
|
14884
|
+
return "Уведомления Уфанет включены. Когда в CLI придет новый вызов, можно ответить `да`, чтобы открыть, или `нет`, чтобы отменить.";
|
|
14885
|
+
}
|
|
14886
|
+
if (/(уведом|оповещ|сообщ).{0,40}(выключ|отключ|off|выкл)/iu.test(normalized) || /(выключ|отключ).{0,40}(уведом|оповещ|сообщ).{0,40}(домофон)/iu.test(normalized)) {
|
|
14887
|
+
await setUfanetNotifications(false);
|
|
14888
|
+
return "Уведомления Уфанет выключены.";
|
|
14889
|
+
}
|
|
14890
|
+
if (/(статус|подключ|аккаунт|договор)/iu.test(normalized)) {
|
|
14891
|
+
const status = await getUfanetStatus();
|
|
14892
|
+
return `Уфанет: ${status.configured ? "настроен" : "не настроен"}, уведомления ${status.notifications}.`;
|
|
14893
|
+
}
|
|
14894
|
+
if (/(открой|открыть|открывай|пусти|впусти|двер)/iu.test(normalized)) {
|
|
14895
|
+
const id = extractUfanetIntercomId(question);
|
|
14896
|
+
const result = await ufanetOpenSmart({ id, state: context.state });
|
|
14897
|
+
return formatUfanetSmartOpenResult(result);
|
|
14898
|
+
}
|
|
14899
|
+
if (/(истори|звонк|кто звонил|последн)/iu.test(normalized)) {
|
|
14900
|
+
const rows = await ufanetGetCallHistory({ page: 1, pageSize: 10 });
|
|
14901
|
+
if (!rows.results?.length) return "В истории Уфанет звонков не найдено.";
|
|
14902
|
+
return ["История звонков Уфанет:", ...rows.results.map((row, index) => `${index + 1}. ${row.calledAt || "-"} — ${row.address || "-"}${row.uuid ? `, UUID ${row.uuid}` : ""}`)].join("\n");
|
|
14903
|
+
}
|
|
14904
|
+
if (/(камер|rtsp|видео)/iu.test(normalized)) {
|
|
14905
|
+
const rows = await ufanetGetCameras();
|
|
14906
|
+
if (!rows.length) return "Камеры Уфанет не найдены.";
|
|
14907
|
+
return ["Камеры Уфанет:", ...rows.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || row.number || "камера"} — ${row.address || "-"}${row.rtspUrl ? `, ${row.rtspUrl}` : ""}`)].join("\n");
|
|
14908
|
+
}
|
|
14909
|
+
const intercoms = await ufanetGetIntercoms();
|
|
14910
|
+
if (!intercoms.length) return "Доступные домофоны Уфанет не найдены.";
|
|
14911
|
+
return ["Доступные домофоны Уфанет:", ...intercoms.map((item, index) => `${index + 1}. ${item.address || item.name || `Домофон #${item.id}`} — ID ${item.id}`)].join("\n");
|
|
14912
|
+
}
|
|
14913
|
+
|
|
14914
|
+
function extractSecondsFromText(text) {
|
|
14915
|
+
const match = String(text || "").match(/(\d{1,3})\s*(?:сек|seconds|s)\b/iu);
|
|
14916
|
+
return match ? Number(match[1]) : 0;
|
|
14917
|
+
}
|
|
14918
|
+
|
|
14564
14919
|
function detectDirectDataFields(normalizedQuestion) {
|
|
14565
14920
|
const fields = [];
|
|
14566
14921
|
if (/(директ|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
|
|
@@ -15179,8 +15534,7 @@ function inferToolPlan(question, options = {}) {
|
|
|
15179
15534
|
if (/(ростелеком|rostelecom)/iu.test(normalized)) return { directAnswer: "Мой домофон Ростелеком пока в разработке. Сейчас реализован Уфанет: /ufanet." };
|
|
15180
15535
|
if (/(открой|открыть|пусти|впусти|двер)/iu.test(normalized)) {
|
|
15181
15536
|
const id = extractUfanetIntercomId(question);
|
|
15182
|
-
|
|
15183
|
-
return { steps: [{ tool: "ufanet_open_intercom", args: { id, confirm: true } }] };
|
|
15537
|
+
return { steps: [{ tool: "ufanet_open_intercom", args: { ...(id ? { id } : {}), confirm: true } }] };
|
|
15184
15538
|
}
|
|
15185
15539
|
if (/(истори|звонк|кто\s+звонил|последн)/iu.test(normalized) && !/(ссылк|запис|видео)/iu.test(normalized)) {
|
|
15186
15540
|
return { steps: [{ tool: "ufanet_call_history", args: { limit: 10 } }] };
|
|
@@ -15687,7 +16041,7 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
15687
16041
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
15688
16042
|
} else if (UFANET_TOOLS.includes(step.tool)) {
|
|
15689
16043
|
await assertPermission("externalApi");
|
|
15690
|
-
const result = await executeUfanetTool(step.tool, step.args || {});
|
|
16044
|
+
const result = await executeUfanetTool(step.tool, { ...(step.args || {}), state: options.state });
|
|
15691
16045
|
current = Array.isArray(result) ? result : [result];
|
|
15692
16046
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
15693
16047
|
} else if (USER_SKILL_TOOLS.includes(step.tool)) {
|
|
@@ -15845,6 +16199,9 @@ function formatToolResult(result, options) {
|
|
|
15845
16199
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
15846
16200
|
}
|
|
15847
16201
|
if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
|
|
16202
|
+
if (row.type === "select" && Array.isArray(row.choices)) return formatUfanetChoicePrompt(row.choices);
|
|
16203
|
+
if (row.type === "empty") return "Уфанет подключен, но доступных домофонов не найдено.";
|
|
16204
|
+
if (row.type === "opened" && row.result) return formatUfanetOpenResult(row.result, row.choice || {});
|
|
15848
16205
|
if (row.provider === "ufanet" && (row.status === "opened" || row.status === "not-opened")) return `Уфанет: домофон #${row.id} ${row.status === "opened" ? "открыт" : "не открылся"}.`;
|
|
15849
16206
|
if (row.provider === "ufanet" && row.uuid && (row.url || row.preview)) return `Уфанет: запись звонка ${row.uuid}\nСсылка: ${row.url || "-"}\nПревью: ${row.preview || "-"}`;
|
|
15850
16207
|
if (row.rtspUrl) return `Камера Уфанет ${row.title || row.number}: ${row.address || "-"}\nRTSP: ${row.rtspUrl}`;
|
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,12 +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
|
|
15
16
|
iola ufanet links UUID
|
|
17
|
+
iola ufanet open
|
|
16
18
|
iola ufanet open ID
|
|
17
19
|
iola ufanet watch
|
|
18
20
|
iola ufanet notifications on
|
|
@@ -21,6 +23,8 @@ iola ufanet notifications status
|
|
|
21
23
|
iola ufanet delete
|
|
22
24
|
```
|
|
23
25
|
|
|
26
|
+
В интерактивном CLI команда `/ufanet` открывает отдельное меню домофона. В нем можно выбрать действие цифрой: открыть домофон, посмотреть список, историю, уведомления, камеры или статус.
|
|
27
|
+
|
|
24
28
|
При настройке нужны:
|
|
25
29
|
|
|
26
30
|
- номер договора Уфанет;
|
|
@@ -42,6 +46,8 @@ UFANET_PASSWORD
|
|
|
42
46
|
Доступные сценарии:
|
|
43
47
|
|
|
44
48
|
- показать доступные домофоны;
|
|
49
|
+
- открыть единственный домофон простой фразой `открой домофон`;
|
|
50
|
+
- если домофонов несколько, показать адреса с цифрами и открыть выбранный номер;
|
|
45
51
|
- открыть домофон по ID;
|
|
46
52
|
- показать историю звонков;
|
|
47
53
|
- получить ссылку на запись звонка по UUID;
|
|
@@ -51,7 +57,7 @@ UFANET_PASSWORD
|
|
|
51
57
|
- включить или выключить настройку уведомлений;
|
|
52
58
|
- удалить локальное подключение.
|
|
53
59
|
|
|
54
|
-
Открытие двери всегда требует явного
|
|
60
|
+
Открытие двери всегда требует явного действия пользователя. Если домофон один, фраза `открой домофон` считается таким действием. Если домофонов несколько, CLI выводит адреса с цифрами, а выбранная цифра считается подтверждением.
|
|
55
61
|
|
|
56
62
|
Уведомления сейчас работают через периодическую проверку истории звонков Уфанета:
|
|
57
63
|
|
|
@@ -59,7 +65,7 @@ UFANET_PASSWORD
|
|
|
59
65
|
iola ufanet watch --seconds 10
|
|
60
66
|
```
|
|
61
67
|
|
|
62
|
-
Остановка режима наблюдения: `Ctrl+C`. Команда `notifications on` сохраняет
|
|
68
|
+
Остановка режима наблюдения: `Ctrl+C`. Команда `notifications on` сохраняет настройку. В обычном интерактивном CLI при включенных уведомлениях новый вызов выводится с вопросом `Открыть?`; ответы `да`, `да открой`, `открой`, `ок` открывают сопоставленный домофон, `нет` или `отмена` отменяют.
|
|
63
69
|
|
|
64
70
|
## Дом.ру
|
|
65
71
|
|