@iola_adm/iola-cli 0.2.47 → 0.2.48
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 +2 -1
- package/package.json +1 -1
- package/skills/ufanet-intercom/SKILL.md +4 -2
- package/src/cli.js +269 -12
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +1 -0
- package/wiki//320/234/320/276/320/271-/320/264/320/276/320/274/320/276/321/204/320/276/320/275.md +5 -2
package/README.md
CHANGED
|
@@ -212,6 +212,7 @@ Yandex tools уже доступны: профиль Yandex ID, расширен
|
|
|
212
212
|
```bash
|
|
213
213
|
iola ufanet setup
|
|
214
214
|
iola ufanet intercoms
|
|
215
|
+
iola ufanet open
|
|
215
216
|
iola ufanet open ID
|
|
216
217
|
iola ufanet history
|
|
217
218
|
iola ufanet cameras
|
|
@@ -222,7 +223,7 @@ iola dom_ru
|
|
|
222
223
|
iola rostelecom
|
|
223
224
|
```
|
|
224
225
|
|
|
225
|
-
Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков, камеры/RTSP и уведомления о новых вызовах через опрос истории звонков, пока CLI открыт. Дом.ру и Ростелеком добавлены как видимые направления `в разработке`.
|
|
226
|
+
Уфанет поддерживается как рабочий провайдер: список домофонов, открытие двери после подтверждения, история звонков, записи звонков, камеры/RTSP и уведомления о новых вызовах через опрос истории звонков, пока CLI открыт. Команда `открой домофон` открывает единственный домофон сразу; если домофонов несколько, CLI просит выбрать адрес цифрой. При уведомлении о вызове можно ответить `да`, `да открой` или `нет`. Дом.ру и Ростелеком добавлены как видимые направления `в разработке`.
|
|
226
227
|
|
|
227
228
|
Инструкция: [Мой домофон](https://github.com/adm-iola/iola-cli/wiki/Мой-домофон).
|
|
228
229
|
|
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,7 +1282,8 @@ async function startAgentRawInput() {
|
|
|
1280
1282
|
|
|
1281
1283
|
async function handleAgentLine(line, state) {
|
|
1282
1284
|
if (!line.startsWith("/")) {
|
|
1283
|
-
const
|
|
1285
|
+
const pendingAnswer = await handlePendingAgentAction(line, state);
|
|
1286
|
+
const answer = pendingAnswer || await aiAsk(state.rawMode ? [line, "--quiet"] : [line], { history: state.history, state });
|
|
1284
1287
|
state.history.push({ role: "user", content: line });
|
|
1285
1288
|
state.history.push({ role: "assistant", content: answer });
|
|
1286
1289
|
if (state.rawMode) state.pendingOutput = answer;
|
|
@@ -1600,6 +1603,21 @@ async function handleAgentLine(line, state) {
|
|
|
1600
1603
|
return false;
|
|
1601
1604
|
}
|
|
1602
1605
|
|
|
1606
|
+
if (command === "ufanet") {
|
|
1607
|
+
await handleUfanet(args.length ? args : ["menu"], state);
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (command === "dom_ru" || command === "domru") {
|
|
1612
|
+
await handleDomRu(args);
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (command === "rostelecom") {
|
|
1617
|
+
await handleRostelecom(args);
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1603
1621
|
const mapped = {
|
|
1604
1622
|
health: ["health", args],
|
|
1605
1623
|
doctor: ["doctor", args],
|
|
@@ -1615,10 +1633,6 @@ async function handleAgentLine(line, state) {
|
|
|
1615
1633
|
files: ["files", args],
|
|
1616
1634
|
archive: ["archive", args],
|
|
1617
1635
|
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
1636
|
changes: ["changes", args],
|
|
1623
1637
|
index: ["index", args],
|
|
1624
1638
|
reports: ["reports", args],
|
|
@@ -1901,6 +1915,131 @@ function flushPendingAgentOutput(state) {
|
|
|
1901
1915
|
printAiAnswer(text);
|
|
1902
1916
|
}
|
|
1903
1917
|
|
|
1918
|
+
async function handlePendingAgentAction(line, state) {
|
|
1919
|
+
const action = state?.pendingAction;
|
|
1920
|
+
if (!action) return "";
|
|
1921
|
+
const text = String(line || "").trim();
|
|
1922
|
+
const normalized = text.toLocaleLowerCase("ru-RU");
|
|
1923
|
+
if (/^(нет|не|отмена|cancel|no|n)$/iu.test(normalized)) {
|
|
1924
|
+
state.pendingAction = null;
|
|
1925
|
+
return "Действие отменено.";
|
|
1926
|
+
}
|
|
1927
|
+
if (action.type === "ufanet_select_open") {
|
|
1928
|
+
const number = Number(text.match(/\d+/u)?.[0] || 0);
|
|
1929
|
+
if (!number || number < 1 || number > action.choices.length) {
|
|
1930
|
+
return formatUfanetChoicePrompt(action.choices);
|
|
1931
|
+
}
|
|
1932
|
+
const choice = action.choices[number - 1];
|
|
1933
|
+
state.pendingAction = null;
|
|
1934
|
+
const result = await ufanetOpenIntercom(choice.id, { confirm: true });
|
|
1935
|
+
return formatUfanetOpenResult(result, choice);
|
|
1936
|
+
}
|
|
1937
|
+
if (action.type === "ufanet_confirm_open") {
|
|
1938
|
+
if (!/^(да|д|yes|y|ok|ок|открой|да\s+открой|открывай|пусти)/iu.test(normalized)) {
|
|
1939
|
+
return "Ответьте `да`, чтобы открыть домофон, или `нет`, чтобы отменить.";
|
|
1940
|
+
}
|
|
1941
|
+
state.pendingAction = null;
|
|
1942
|
+
const result = await ufanetOpenIntercom(action.id, { confirm: true });
|
|
1943
|
+
return formatUfanetOpenResult(result, action);
|
|
1944
|
+
}
|
|
1945
|
+
return "";
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function setupAgentUfanetNotifications(state, render) {
|
|
1949
|
+
if (!input.isTTY) return () => {};
|
|
1950
|
+
let stopped = false;
|
|
1951
|
+
let timer = null;
|
|
1952
|
+
let busy = false;
|
|
1953
|
+
const schedule = (seconds = 10) => {
|
|
1954
|
+
if (stopped) return;
|
|
1955
|
+
timer = setTimeout(loop, normalizeUfanetPollInterval(seconds) * 1000);
|
|
1956
|
+
};
|
|
1957
|
+
const loop = async () => {
|
|
1958
|
+
if (stopped || busy) {
|
|
1959
|
+
schedule(10);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
busy = true;
|
|
1963
|
+
let nextInterval = 10;
|
|
1964
|
+
try {
|
|
1965
|
+
const config = await loadConfig();
|
|
1966
|
+
const notificationConfig = config.domophones?.providers?.ufanet?.notifications || {};
|
|
1967
|
+
nextInterval = normalizeUfanetPollInterval(notificationConfig.intervalSeconds || 10);
|
|
1968
|
+
if (notificationConfig.enabled) {
|
|
1969
|
+
const history = await ufanetGetCallHistory({ page: 1, pageSize: 10 });
|
|
1970
|
+
const rows = history.results || [];
|
|
1971
|
+
let lastSeen = notificationConfig.lastSeen || "";
|
|
1972
|
+
if (!lastSeen && rows[0]) {
|
|
1973
|
+
await updateUfanetLastSeen(ufanetCallKey(rows[0]));
|
|
1974
|
+
} else {
|
|
1975
|
+
const fresh = [];
|
|
1976
|
+
for (const row of rows) {
|
|
1977
|
+
const key = ufanetCallKey(row);
|
|
1978
|
+
if (!key || key === lastSeen) break;
|
|
1979
|
+
fresh.push(row);
|
|
1980
|
+
}
|
|
1981
|
+
if (fresh.length > 0) {
|
|
1982
|
+
for (const row of fresh.reverse()) {
|
|
1983
|
+
await announceUfanetCallInAgent(row, state, render);
|
|
1984
|
+
}
|
|
1985
|
+
await updateUfanetLastSeen(ufanetCallKey(rows[0]) || lastSeen);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
if (process.env.IOLA_DEBUG === "1") {
|
|
1991
|
+
printAgentAsyncMessage(state, render, `Уфанет: ошибка проверки вызовов: ${error instanceof Error ? error.message : String(error)}`);
|
|
1992
|
+
}
|
|
1993
|
+
} finally {
|
|
1994
|
+
busy = false;
|
|
1995
|
+
schedule(nextInterval);
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
schedule(2);
|
|
1999
|
+
return () => {
|
|
2000
|
+
stopped = true;
|
|
2001
|
+
if (timer) clearTimeout(timer);
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
async function announceUfanetCallInAgent(row, state, render) {
|
|
2006
|
+
const match = await resolveUfanetIntercomForCall(row).catch(() => null);
|
|
2007
|
+
if (match?.id) {
|
|
2008
|
+
state.pendingAction = {
|
|
2009
|
+
type: "ufanet_confirm_open",
|
|
2010
|
+
id: match.id,
|
|
2011
|
+
address: match.address || row.address || "",
|
|
2012
|
+
name: match.name || "",
|
|
2013
|
+
createdAt: new Date().toISOString(),
|
|
2014
|
+
};
|
|
2015
|
+
printAgentAsyncMessage(state, render, [
|
|
2016
|
+
"Новый вызов домофона Уфанет:",
|
|
2017
|
+
`Адрес: ${row.address || match.address || "-"}`,
|
|
2018
|
+
`Время: ${row.calledAt || "-"}`,
|
|
2019
|
+
"Открыть? Ответьте `да` или `нет`.",
|
|
2020
|
+
].join("\n"));
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const intercoms = await ufanetGetIntercoms().catch(() => []);
|
|
2024
|
+
const choices = intercoms.map((item) => ({ id: item.id, name: item.name, address: item.address }));
|
|
2025
|
+
state.pendingAction = choices.length
|
|
2026
|
+
? { type: "ufanet_select_open", choices, createdAt: new Date().toISOString() }
|
|
2027
|
+
: null;
|
|
2028
|
+
printAgentAsyncMessage(state, render, [
|
|
2029
|
+
"Новый вызов домофона Уфанет:",
|
|
2030
|
+
`Адрес: ${row.address || "-"}`,
|
|
2031
|
+
`Время: ${row.calledAt || "-"}`,
|
|
2032
|
+
choices.length ? "Открыть? Выберите домофон номером:" : "Не смог сопоставить вызов с домофоном.",
|
|
2033
|
+
choices.length ? formatUfanetChoicePrompt(choices) : "",
|
|
2034
|
+
].filter(Boolean).join("\n"));
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function printAgentAsyncMessage(state, render, text) {
|
|
2038
|
+
clearAgentInputArea(state);
|
|
2039
|
+
output.write(`${text}\n`);
|
|
2040
|
+
render();
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1904
2043
|
function colorSlashSelection(row) {
|
|
1905
2044
|
if (!output.isTTY || process.env.NO_COLOR === "1") return row;
|
|
1906
2045
|
return `\x1b[38;5;213m${row}\x1b[0m`;
|
|
@@ -3571,7 +3710,7 @@ async function handleRostelecom() {
|
|
|
3571
3710
|
console.log("Пока доступна заготовка пункта в городских сервисах. API/авторизация будут добавлены после исследования провайдера.");
|
|
3572
3711
|
}
|
|
3573
3712
|
|
|
3574
|
-
async function handleUfanet(args = []) {
|
|
3713
|
+
async function handleUfanet(args = [], agentState = null) {
|
|
3575
3714
|
const [action = process.stdin.isTTY ? "menu" : "status", target, ...rest] = args;
|
|
3576
3715
|
const options = parseOptions(rest);
|
|
3577
3716
|
|
|
@@ -3598,7 +3737,11 @@ async function handleUfanet(args = []) {
|
|
|
3598
3737
|
|
|
3599
3738
|
if (action === "open") {
|
|
3600
3739
|
const intercomId = target || options.id || options.intercom;
|
|
3601
|
-
if (!intercomId)
|
|
3740
|
+
if (!intercomId) {
|
|
3741
|
+
const result = await ufanetOpenSmart({ state: agentState });
|
|
3742
|
+
console.log(formatUfanetSmartOpenResult(result));
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3602
3745
|
const ok = options.yes || options.confirm || await confirm(`Открыть домофон Уфанет #${intercomId}? [y/N] `);
|
|
3603
3746
|
if (!ok) {
|
|
3604
3747
|
console.log("Открытие отменено.");
|
|
@@ -3917,7 +4060,9 @@ function ufanetCallKey(row = {}) {
|
|
|
3917
4060
|
async function executeUfanetTool(tool, args = {}) {
|
|
3918
4061
|
if (tool === "ufanet_status") return getUfanetStatus();
|
|
3919
4062
|
if (tool === "ufanet_intercoms") return ufanetGetIntercoms();
|
|
3920
|
-
if (tool === "ufanet_open_intercom") return
|
|
4063
|
+
if (tool === "ufanet_open_intercom") return args.id || args.intercomId || args.intercom_id
|
|
4064
|
+
? ufanetOpenIntercom(args.id || args.intercomId || args.intercom_id, args)
|
|
4065
|
+
: ufanetOpenSmart({ ...args, state: args.state });
|
|
3921
4066
|
if (tool === "ufanet_call_history") return ufanetGetCallHistory({ page: args.page || 1, pageSize: args.pageSize || args.page_size || args.limit || 10 });
|
|
3922
4067
|
if (tool === "ufanet_call_links") return ufanetGetCallLinks(args.uuid || args.id);
|
|
3923
4068
|
if (tool === "ufanet_cameras") return ufanetGetCameras();
|
|
@@ -4011,6 +4156,59 @@ async function ufanetOpenIntercom(intercomId, options = {}) {
|
|
|
4011
4156
|
return { provider: "ufanet", status: payload?.result ? "opened" : "not-opened", id: Number(intercomId), result: Boolean(payload?.result) };
|
|
4012
4157
|
}
|
|
4013
4158
|
|
|
4159
|
+
async function ufanetOpenSmart(options = {}) {
|
|
4160
|
+
const intercomId = options.id || options.intercomId || options.intercom_id;
|
|
4161
|
+
if (intercomId) {
|
|
4162
|
+
const result = await ufanetOpenIntercom(intercomId, { confirm: true });
|
|
4163
|
+
return { type: "opened", result, choice: { id: intercomId } };
|
|
4164
|
+
}
|
|
4165
|
+
const intercoms = await ufanetGetIntercoms();
|
|
4166
|
+
if (intercoms.length === 0) return { type: "empty" };
|
|
4167
|
+
const choices = intercoms.map((item) => ({ id: item.id, name: item.name, address: item.address }));
|
|
4168
|
+
if (intercoms.length === 1) {
|
|
4169
|
+
const result = await ufanetOpenIntercom(intercoms[0].id, { confirm: true });
|
|
4170
|
+
return { type: "opened", result, choice: choices[0] };
|
|
4171
|
+
}
|
|
4172
|
+
if (options.state) {
|
|
4173
|
+
options.state.pendingAction = { type: "ufanet_select_open", choices, createdAt: new Date().toISOString() };
|
|
4174
|
+
}
|
|
4175
|
+
return { type: "select", choices };
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
function formatUfanetSmartOpenResult(value) {
|
|
4179
|
+
if (value.type === "empty") return "Уфанет подключен, но доступных домофонов не найдено.";
|
|
4180
|
+
if (value.type === "select") return formatUfanetChoicePrompt(value.choices);
|
|
4181
|
+
if (value.type === "opened") return formatUfanetOpenResult(value.result, value.choice);
|
|
4182
|
+
return "Не удалось выполнить действие с домофоном.";
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
function formatUfanetChoicePrompt(choices = []) {
|
|
4186
|
+
return [
|
|
4187
|
+
"Найдено несколько домофонов. Какой открыть? Ответьте цифрой:",
|
|
4188
|
+
...choices.map((item, index) => `${index + 1}. ${item.address || item.name || `Домофон #${item.id}`} — ID ${item.id}`),
|
|
4189
|
+
].join("\n");
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
function formatUfanetOpenResult(result, choice = {}) {
|
|
4193
|
+
const label = choice.address || choice.name || `ID ${result.id}`;
|
|
4194
|
+
return result.result
|
|
4195
|
+
? `Домофон открыт: ${label}.`
|
|
4196
|
+
: `Уфанет не подтвердил открытие домофона: ${label}.`;
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
async function resolveUfanetIntercomForCall(row = {}) {
|
|
4200
|
+
const address = normalizeGeoText(row.address || "");
|
|
4201
|
+
if (!address) return null;
|
|
4202
|
+
const intercoms = await ufanetGetIntercoms();
|
|
4203
|
+
const matches = intercoms.filter((item) => {
|
|
4204
|
+
const itemAddress = normalizeGeoText(item.address || item.name || "");
|
|
4205
|
+
return itemAddress && (itemAddress.includes(address) || address.includes(itemAddress));
|
|
4206
|
+
});
|
|
4207
|
+
if (matches.length === 1) return matches[0];
|
|
4208
|
+
if (matches.length > 1) return null;
|
|
4209
|
+
return intercoms.length === 1 ? intercoms[0] : null;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4014
4212
|
async function ufanetGetCallHistory(options = {}) {
|
|
4015
4213
|
const page = Math.max(1, Number(options.page || 1));
|
|
4016
4214
|
const pageSize = Math.max(1, Math.min(100, Number(options.pageSize || 10)));
|
|
@@ -12915,6 +13113,7 @@ async function setupIolaLocal(args) {
|
|
|
12915
13113
|
|
|
12916
13114
|
async function aiAsk(args, context = {}) {
|
|
12917
13115
|
const options = parseOptions(args);
|
|
13116
|
+
if (context.state) options.state = context.state;
|
|
12918
13117
|
const question = options._.join(" ").trim();
|
|
12919
13118
|
|
|
12920
13119
|
if (!question) {
|
|
@@ -12964,6 +13163,20 @@ async function aiAsk(args, context = {}) {
|
|
|
12964
13163
|
if (!options.quiet) console.log(userSkillAnswer);
|
|
12965
13164
|
return userSkillAnswer;
|
|
12966
13165
|
}
|
|
13166
|
+
const ufanetAnswer = await buildUfanetDirectAnswer(question, context);
|
|
13167
|
+
if (ufanetAnswer) {
|
|
13168
|
+
if (historyEnabled) {
|
|
13169
|
+
recordAskHistory({ question, answer: ufanetAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
13170
|
+
appendSessionExchange(sessionId, question, ufanetAnswer, dataContext, "");
|
|
13171
|
+
}
|
|
13172
|
+
emitEvent(options, "answer", { length: ufanetAnswer.length, sessionId, direct: true, ufanet: true });
|
|
13173
|
+
if (options.output) {
|
|
13174
|
+
await assertPermission("writeFiles");
|
|
13175
|
+
await writeFile(options.output, ufanetAnswer, "utf8");
|
|
13176
|
+
}
|
|
13177
|
+
if (!options.quiet) console.log(ufanetAnswer);
|
|
13178
|
+
return ufanetAnswer;
|
|
13179
|
+
}
|
|
12967
13180
|
if (/(контакт|адресн)/iu.test(question) && !isExplicitYandexDiskPathDelete(question)) {
|
|
12968
13181
|
const yandexContactAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
12969
13182
|
if (yandexContactAnswer) {
|
|
@@ -14561,6 +14774,48 @@ function cleanupCloudSaveText(question) {
|
|
|
14561
14774
|
.trim();
|
|
14562
14775
|
}
|
|
14563
14776
|
|
|
14777
|
+
async function buildUfanetDirectAnswer(question, context = {}) {
|
|
14778
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
14779
|
+
if (!/(домофон|уфанет|ufanet|дверь|подъезд)/iu.test(normalized)) return "";
|
|
14780
|
+
if (/(дом\.?ру|dom\.?ru)/iu.test(normalized)) return "Мой домофон Дом.ру пока в разработке. Сейчас реализован Уфанет: /ufanet.";
|
|
14781
|
+
if (/(ростелеком|rostelecom)/iu.test(normalized)) return "Мой домофон Ростелеком пока в разработке. Сейчас реализован Уфанет: /ufanet.";
|
|
14782
|
+
if (/(уведом|оповещ|сообщ).{0,40}(включ|получ|on|вкл)/iu.test(normalized) || /(включ|получ).{0,40}(уведом|оповещ|сообщ).{0,40}(домофон)/iu.test(normalized)) {
|
|
14783
|
+
await setUfanetNotifications(true, { intervalSeconds: extractSecondsFromText(question) || 10 });
|
|
14784
|
+
return "Уведомления Уфанет включены. Когда в CLI придет новый вызов, можно ответить `да`, чтобы открыть, или `нет`, чтобы отменить.";
|
|
14785
|
+
}
|
|
14786
|
+
if (/(уведом|оповещ|сообщ).{0,40}(выключ|отключ|off|выкл)/iu.test(normalized) || /(выключ|отключ).{0,40}(уведом|оповещ|сообщ).{0,40}(домофон)/iu.test(normalized)) {
|
|
14787
|
+
await setUfanetNotifications(false);
|
|
14788
|
+
return "Уведомления Уфанет выключены.";
|
|
14789
|
+
}
|
|
14790
|
+
if (/(статус|подключ|аккаунт|договор)/iu.test(normalized)) {
|
|
14791
|
+
const status = await getUfanetStatus();
|
|
14792
|
+
return `Уфанет: ${status.configured ? "настроен" : "не настроен"}, уведомления ${status.notifications}.`;
|
|
14793
|
+
}
|
|
14794
|
+
if (/(открой|открыть|открывай|пусти|впусти|двер)/iu.test(normalized)) {
|
|
14795
|
+
const id = extractUfanetIntercomId(question);
|
|
14796
|
+
const result = await ufanetOpenSmart({ id, state: context.state });
|
|
14797
|
+
return formatUfanetSmartOpenResult(result);
|
|
14798
|
+
}
|
|
14799
|
+
if (/(истори|звонк|кто звонил|последн)/iu.test(normalized)) {
|
|
14800
|
+
const rows = await ufanetGetCallHistory({ page: 1, pageSize: 10 });
|
|
14801
|
+
if (!rows.results?.length) return "В истории Уфанет звонков не найдено.";
|
|
14802
|
+
return ["История звонков Уфанет:", ...rows.results.map((row, index) => `${index + 1}. ${row.calledAt || "-"} — ${row.address || "-"}${row.uuid ? `, UUID ${row.uuid}` : ""}`)].join("\n");
|
|
14803
|
+
}
|
|
14804
|
+
if (/(камер|rtsp|видео)/iu.test(normalized)) {
|
|
14805
|
+
const rows = await ufanetGetCameras();
|
|
14806
|
+
if (!rows.length) return "Камеры Уфанет не найдены.";
|
|
14807
|
+
return ["Камеры Уфанет:", ...rows.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || row.number || "камера"} — ${row.address || "-"}${row.rtspUrl ? `, ${row.rtspUrl}` : ""}`)].join("\n");
|
|
14808
|
+
}
|
|
14809
|
+
const intercoms = await ufanetGetIntercoms();
|
|
14810
|
+
if (!intercoms.length) return "Доступные домофоны Уфанет не найдены.";
|
|
14811
|
+
return ["Доступные домофоны Уфанет:", ...intercoms.map((item, index) => `${index + 1}. ${item.address || item.name || `Домофон #${item.id}`} — ID ${item.id}`)].join("\n");
|
|
14812
|
+
}
|
|
14813
|
+
|
|
14814
|
+
function extractSecondsFromText(text) {
|
|
14815
|
+
const match = String(text || "").match(/(\d{1,3})\s*(?:сек|seconds|s)\b/iu);
|
|
14816
|
+
return match ? Number(match[1]) : 0;
|
|
14817
|
+
}
|
|
14818
|
+
|
|
14564
14819
|
function detectDirectDataFields(normalizedQuestion) {
|
|
14565
14820
|
const fields = [];
|
|
14566
14821
|
if (/(директ|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
|
|
@@ -15179,8 +15434,7 @@ function inferToolPlan(question, options = {}) {
|
|
|
15179
15434
|
if (/(ростелеком|rostelecom)/iu.test(normalized)) return { directAnswer: "Мой домофон Ростелеком пока в разработке. Сейчас реализован Уфанет: /ufanet." };
|
|
15180
15435
|
if (/(открой|открыть|пусти|впусти|двер)/iu.test(normalized)) {
|
|
15181
15436
|
const id = extractUfanetIntercomId(question);
|
|
15182
|
-
|
|
15183
|
-
return { steps: [{ tool: "ufanet_open_intercom", args: { id, confirm: true } }] };
|
|
15437
|
+
return { steps: [{ tool: "ufanet_open_intercom", args: { ...(id ? { id } : {}), confirm: true } }] };
|
|
15184
15438
|
}
|
|
15185
15439
|
if (/(истори|звонк|кто\s+звонил|последн)/iu.test(normalized) && !/(ссылк|запис|видео)/iu.test(normalized)) {
|
|
15186
15440
|
return { steps: [{ tool: "ufanet_call_history", args: { limit: 10 } }] };
|
|
@@ -15687,7 +15941,7 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
15687
15941
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
15688
15942
|
} else if (UFANET_TOOLS.includes(step.tool)) {
|
|
15689
15943
|
await assertPermission("externalApi");
|
|
15690
|
-
const result = await executeUfanetTool(step.tool, step.args || {});
|
|
15944
|
+
const result = await executeUfanetTool(step.tool, { ...(step.args || {}), state: options.state });
|
|
15691
15945
|
current = Array.isArray(result) ? result : [result];
|
|
15692
15946
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
15693
15947
|
} else if (USER_SKILL_TOOLS.includes(step.tool)) {
|
|
@@ -15845,6 +16099,9 @@ function formatToolResult(result, options) {
|
|
|
15845
16099
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
15846
16100
|
}
|
|
15847
16101
|
if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
|
|
16102
|
+
if (row.type === "select" && Array.isArray(row.choices)) return formatUfanetChoicePrompt(row.choices);
|
|
16103
|
+
if (row.type === "empty") return "Уфанет подключен, но доступных домофонов не найдено.";
|
|
16104
|
+
if (row.type === "opened" && row.result) return formatUfanetOpenResult(row.result, row.choice || {});
|
|
15848
16105
|
if (row.provider === "ufanet" && (row.status === "opened" || row.status === "not-opened")) return `Уфанет: домофон #${row.id} ${row.status === "opened" ? "открыт" : "не открылся"}.`;
|
|
15849
16106
|
if (row.provider === "ufanet" && row.uuid && (row.url || row.preview)) return `Уфанет: запись звонка ${row.uuid}\nСсылка: ${row.url || "-"}\nПревью: ${row.preview || "-"}`;
|
|
15850
16107
|
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
|
@@ -13,6 +13,7 @@ iola ufanet intercoms
|
|
|
13
13
|
iola ufanet history
|
|
14
14
|
iola ufanet cameras
|
|
15
15
|
iola ufanet links UUID
|
|
16
|
+
iola ufanet open
|
|
16
17
|
iola ufanet open ID
|
|
17
18
|
iola ufanet watch
|
|
18
19
|
iola ufanet notifications on
|
|
@@ -42,6 +43,8 @@ UFANET_PASSWORD
|
|
|
42
43
|
Доступные сценарии:
|
|
43
44
|
|
|
44
45
|
- показать доступные домофоны;
|
|
46
|
+
- открыть единственный домофон простой фразой `открой домофон`;
|
|
47
|
+
- если домофонов несколько, показать адреса с цифрами и открыть выбранный номер;
|
|
45
48
|
- открыть домофон по ID;
|
|
46
49
|
- показать историю звонков;
|
|
47
50
|
- получить ссылку на запись звонка по UUID;
|
|
@@ -51,7 +54,7 @@ UFANET_PASSWORD
|
|
|
51
54
|
- включить или выключить настройку уведомлений;
|
|
52
55
|
- удалить локальное подключение.
|
|
53
56
|
|
|
54
|
-
Открытие двери всегда требует явного
|
|
57
|
+
Открытие двери всегда требует явного действия пользователя. Если домофон один, фраза `открой домофон` считается таким действием. Если домофонов несколько, CLI выводит адреса с цифрами, а выбранная цифра считается подтверждением.
|
|
55
58
|
|
|
56
59
|
Уведомления сейчас работают через периодическую проверку истории звонков Уфанета:
|
|
57
60
|
|
|
@@ -59,7 +62,7 @@ UFANET_PASSWORD
|
|
|
59
62
|
iola ufanet watch --seconds 10
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
Остановка режима наблюдения: `Ctrl+C`. Команда `notifications on` сохраняет
|
|
65
|
+
Остановка режима наблюдения: `Ctrl+C`. Команда `notifications on` сохраняет настройку. В обычном интерактивном CLI при включенных уведомлениях новый вызов выводится с вопросом `Открыть?`; ответы `да`, `да открой`, `открой`, `ок` открывают сопоставленный домофон, `нет` или `отмена` отменяют.
|
|
63
66
|
|
|
64
67
|
## Дом.ру
|
|
65
68
|
|