@iola_adm/iola-cli 0.1.64 → 0.1.66
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 +0 -12
- package/package.json +3 -2
- package/src/cli.js +239 -1012
- package/test/smoke-test.js +64 -0
- package/wiki/Daemon-RPC-/320/270-cron.md +0 -7
- package/wiki/Home.md +0 -1
- package/wiki//320/220/321/200/321/205/320/270/320/262/321/213-/320/270-/320/274/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +1 -1
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +0 -20
- package/wiki//320/234/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +1 -14
- package/skills/gosuslugi/SKILL.md +0 -16
- package/wiki//320/237/320/276/320/264/320/272/320/273/321/216/321/207/320/265/320/275/320/270/320/265-/320/223/320/276/321/201/321/203/321/201/320/273/321/203/320/263.md +0 -138
package/src/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
4
3
|
import { createServer } from "node:http";
|
|
5
4
|
import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -26,31 +25,14 @@ const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
|
|
|
26
25
|
const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
|
|
27
26
|
const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
|
|
28
27
|
const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
|
|
29
|
-
const GOSUSLUGI_BROWSER_PROFILE_DIR = path.join(CONFIG_DIR, "gosuslugi-browser-profile");
|
|
30
|
-
const GOSUSLUGI_BROWSER_LOCK_DIR = path.join(CONFIG_DIR, "gosuslugi-browser-profile.lock");
|
|
31
|
-
const GOSUSLUGI_DEFAULT_URL = "https://www.gosuslugi.ru/";
|
|
32
28
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
33
|
-
const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"
|
|
29
|
+
const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
|
|
34
30
|
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
35
31
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
36
32
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
37
33
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
38
34
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
39
35
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
40
|
-
const GOSUSLUGI_CONSENT_VERSION = "2026-05-26-personal-local-v1";
|
|
41
|
-
const GOSUSLUGI_CONSENT_TEXT = `Подключение личных Госуслуг
|
|
42
|
-
|
|
43
|
-
Вы подключаете личную учетную запись Госуслуг к локальному CLI-агенту iola-cli на этом компьютере.
|
|
44
|
-
|
|
45
|
-
Нажимая "Да", вы подтверждаете, что:
|
|
46
|
-
- используете собственную учетную запись Госуслуг;
|
|
47
|
-
- понимаете, что все действия, выполненные через CLI-агента после подключения, считаются действиями владельца этой учетной записи;
|
|
48
|
-
- разрешаете iola-cli локально сохранить данные доступа, необходимые для повторного входа или выполнения запросов от вашего имени;
|
|
49
|
-
- понимаете, что данные доступа хранятся только на этом компьютере в локальном хранилище пользователя и не передаются разработчикам CLI, администрации города или третьим лицам;
|
|
50
|
-
- обязуетесь не подключать чужие учетные записи и не передавать локальные файлы доступа другим лицам;
|
|
51
|
-
- понимаете, что перед юридически значимыми действиями, отправкой заявлений, оплатой, подписанием или изменением персональных данных CLI должен запросить отдельное подтверждение.
|
|
52
|
-
|
|
53
|
-
Продолжить подключение?`;
|
|
54
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
55
37
|
const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
56
38
|
const USER_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
@@ -131,19 +113,6 @@ const DEFAULT_AI_CONFIG = {
|
|
|
131
113
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
132
114
|
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
133
115
|
},
|
|
134
|
-
gosuslugi: {
|
|
135
|
-
enabled: false,
|
|
136
|
-
mode: "personal-browser",
|
|
137
|
-
authUrl: "",
|
|
138
|
-
tokenUrl: "",
|
|
139
|
-
userinfoUrl: "",
|
|
140
|
-
clientId: "",
|
|
141
|
-
clientSecret: "",
|
|
142
|
-
scope: "openid",
|
|
143
|
-
redirectHost: "127.0.0.1",
|
|
144
|
-
redirectPort: 18791,
|
|
145
|
-
redirectPath: "/gosuslugi/callback",
|
|
146
|
-
},
|
|
147
116
|
ai: {
|
|
148
117
|
activeProfile: "local",
|
|
149
118
|
provider: "ollama",
|
|
@@ -181,9 +150,6 @@ const DEFAULT_AI_CONFIG = {
|
|
|
181
150
|
export_report: true,
|
|
182
151
|
file_read: false,
|
|
183
152
|
browser_open: true,
|
|
184
|
-
gosuslugi_whoami: true,
|
|
185
|
-
gosuslugi_debt: true,
|
|
186
|
-
gosuslugi_notifications: true,
|
|
187
153
|
files_tree: false,
|
|
188
154
|
files_read: false,
|
|
189
155
|
files_search: false,
|
|
@@ -214,7 +180,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
214
180
|
suggestions: true,
|
|
215
181
|
},
|
|
216
182
|
skills: {
|
|
217
|
-
enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"
|
|
183
|
+
enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
|
|
218
184
|
},
|
|
219
185
|
daemon: {
|
|
220
186
|
host: "127.0.0.1",
|
|
@@ -286,11 +252,6 @@ const SLASH_COMMANDS = [
|
|
|
286
252
|
{ command: "/sessions", description: "AI-сессии" },
|
|
287
253
|
{ command: "/resume SESSION_ID", description: "продолжить сессию" },
|
|
288
254
|
{ command: "/features list", description: "feature flags" },
|
|
289
|
-
{ command: "/gosuslugi status", description: "личное подключение Госуслуг" },
|
|
290
|
-
{ command: "/gosuslugi connect", description: "открыть личный вход Госуслуг" },
|
|
291
|
-
{ command: "/gosuslugi debt", description: "задолженности Госуслуг" },
|
|
292
|
-
{ command: "/gosuslugi notifications", description: "уведомления Госуслуг" },
|
|
293
|
-
{ command: "/gosuslugi keepalive", description: "проверка сессии каждые 30 минут" },
|
|
294
255
|
{ command: "/wiki", description: "ссылки на документацию" },
|
|
295
256
|
{ command: "/context list", description: "локальный контекст проекта" },
|
|
296
257
|
{ command: "/skills list", description: "skills" },
|
|
@@ -328,9 +289,14 @@ const SLASH_COMMANDS = [
|
|
|
328
289
|
{ command: "/search лицей --limit 3", description: "поиск" },
|
|
329
290
|
{ command: "/mcp-info", description: "публичный MCP" },
|
|
330
291
|
{ command: "/profiles", description: "AI-профили" },
|
|
292
|
+
{ command: "/model", description: "переключить AI: local/API/Codex" },
|
|
293
|
+
{ command: "/model codex", description: "выбрать модель Codex" },
|
|
294
|
+
{ command: "/model api", description: "выбрать API-модель" },
|
|
331
295
|
{ command: "/models openrouter --search qwen", description: "модели" },
|
|
332
296
|
{ command: "/ai doctor", description: "AI diagnostics" },
|
|
333
297
|
{ command: "/ai setup ollama", description: "настройка Ollama" },
|
|
298
|
+
{ command: "/use codex", description: "выбрать Codex CLI" },
|
|
299
|
+
{ command: "/use local", description: "выбрать локальный профиль" },
|
|
334
300
|
{ command: "/use openai", description: "выбрать OpenAI" },
|
|
335
301
|
{ command: "/use ollama", description: "выбрать Ollama" },
|
|
336
302
|
{ command: "/key status", description: "API-ключи" },
|
|
@@ -361,7 +327,6 @@ const COMMANDS = new Map([
|
|
|
361
327
|
["fork", forkSession],
|
|
362
328
|
["features", handleFeatures],
|
|
363
329
|
["settings", handleSettings],
|
|
364
|
-
["gosuslugi", handleGosuslugi],
|
|
365
330
|
["wiki", handleWiki],
|
|
366
331
|
["context", handleContext],
|
|
367
332
|
["skills", handleSkills],
|
|
@@ -489,7 +454,6 @@ async function showHelp() {
|
|
|
489
454
|
iola agent интерактивный режим
|
|
490
455
|
iola ai setup настройка AI-профиля
|
|
491
456
|
iola browser status браузерный runtime
|
|
492
|
-
iola gosuslugi status личное подключение Госуслуг
|
|
493
457
|
iola mcp status MCP-подключение
|
|
494
458
|
iola doctor диагностика
|
|
495
459
|
iola wiki документация
|
|
@@ -524,7 +488,6 @@ Usage:
|
|
|
524
488
|
iola fork SESSION_ID [TEXT]
|
|
525
489
|
iola features list|enable|disable
|
|
526
490
|
iola settings list|get|validate|doctor|init
|
|
527
|
-
iola gosuslugi terms|consent|status|check|keepalive|install-keepalive|keepalive-status|uninstall-keepalive|connect|open|text|screenshot|whoami|debt|notifications|mark-read|logout|configure|login|userinfo
|
|
528
491
|
iola wiki [open|links]
|
|
529
492
|
iola context list|show|init
|
|
530
493
|
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
@@ -803,10 +766,11 @@ async function startAgentReadline() {
|
|
|
803
766
|
}
|
|
804
767
|
|
|
805
768
|
async function startAgentRawInput() {
|
|
806
|
-
const state = { history: [], buffer: "", selected: 0, slashOpen: false, running: false, renderedInputLines: 0, rawMode: true, pendingOutput: "" };
|
|
769
|
+
const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, rawMode: true, pendingOutput: "", aiStatus: null };
|
|
807
770
|
const wasRaw = input.isRaw;
|
|
808
771
|
activateRawInput(input);
|
|
809
772
|
|
|
773
|
+
await refreshAgentAiStatus(state);
|
|
810
774
|
const render = () => renderAgentInput(state);
|
|
811
775
|
render();
|
|
812
776
|
|
|
@@ -827,13 +791,19 @@ async function startAgentRawInput() {
|
|
|
827
791
|
}
|
|
828
792
|
if (key?.name === "up" && state.slashOpen) {
|
|
829
793
|
const matches = currentSlashMatches(state);
|
|
830
|
-
|
|
794
|
+
const nextSelected = Math.max(0, state.selected - 1);
|
|
795
|
+
state.selected = nextSelected;
|
|
796
|
+
if (state.selected < state.slashOffset) state.slashOffset = state.selected;
|
|
831
797
|
render();
|
|
832
798
|
continue;
|
|
833
799
|
}
|
|
834
800
|
if (key?.name === "down" && state.slashOpen) {
|
|
835
801
|
const matches = currentSlashMatches(state);
|
|
836
|
-
|
|
802
|
+
const visibleLimit = getSlashVisibleLimit();
|
|
803
|
+
const nextSelected = Math.min(matches.length - 1, state.selected + 1);
|
|
804
|
+
state.selected = Math.max(0, nextSelected);
|
|
805
|
+
if (state.selected >= state.slashOffset + visibleLimit) state.slashOffset = state.selected - visibleLimit + 1;
|
|
806
|
+
state.slashOffset = Math.max(0, Math.min(state.slashOffset, Math.max(0, matches.length - visibleLimit)));
|
|
837
807
|
render();
|
|
838
808
|
continue;
|
|
839
809
|
}
|
|
@@ -861,6 +831,7 @@ async function startAgentRawInput() {
|
|
|
861
831
|
const shouldExit = await handleAgentLine(line, state);
|
|
862
832
|
stopActivity();
|
|
863
833
|
flushPendingAgentOutput(state);
|
|
834
|
+
await refreshAgentAiStatus(state);
|
|
864
835
|
if (!shouldExit) restoreRawInput();
|
|
865
836
|
if (shouldExit) break;
|
|
866
837
|
} catch (error) {
|
|
@@ -1059,10 +1030,6 @@ async function handleAgentLine(line, state) {
|
|
|
1059
1030
|
return false;
|
|
1060
1031
|
}
|
|
1061
1032
|
|
|
1062
|
-
if (command === "gosuslugi") {
|
|
1063
|
-
await handleGosuslugi(args);
|
|
1064
|
-
return false;
|
|
1065
|
-
}
|
|
1066
1033
|
|
|
1067
1034
|
if (command === "workspace") {
|
|
1068
1035
|
await handleWorkspace(args);
|
|
@@ -1164,6 +1131,11 @@ async function handleAgentLine(line, state) {
|
|
|
1164
1131
|
return false;
|
|
1165
1132
|
}
|
|
1166
1133
|
|
|
1134
|
+
if (command === "model") {
|
|
1135
|
+
await slashModelMenu(args);
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1167
1139
|
if (command === "use") {
|
|
1168
1140
|
await useAiProvider(args);
|
|
1169
1141
|
return false;
|
|
@@ -1213,7 +1185,6 @@ async function handleAgentLine(line, state) {
|
|
|
1213
1185
|
resume: ["resume", args],
|
|
1214
1186
|
fork: ["fork", args],
|
|
1215
1187
|
features: ["features", args],
|
|
1216
|
-
gosuslugi: ["gosuslugi", args],
|
|
1217
1188
|
wiki: ["wiki", args],
|
|
1218
1189
|
context: ["context", args],
|
|
1219
1190
|
skills: ["skills", args],
|
|
@@ -1279,8 +1250,9 @@ function printAgentHelp() {
|
|
|
1279
1250
|
|
|
1280
1251
|
function printSlashMenu(filter = "", options = {}) {
|
|
1281
1252
|
const normalized = String(filter || "").replace(/^\//, "");
|
|
1253
|
+
const limit = options.limit === undefined ? Infinity : Number(options.limit);
|
|
1282
1254
|
const rows = getSlashCommandMatches(normalized)
|
|
1283
|
-
.slice(0,
|
|
1255
|
+
.slice(0, limit)
|
|
1284
1256
|
.map((item) => ({ command: item.command, description: item.description }));
|
|
1285
1257
|
if (rows.length === 0) {
|
|
1286
1258
|
console.log(`Нет slash-команд по фильтру: ${filter}`);
|
|
@@ -1308,11 +1280,16 @@ function getSlashCommandMatches(filter = "") {
|
|
|
1308
1280
|
function updateSlashState(state) {
|
|
1309
1281
|
state.slashOpen = state.buffer.startsWith("/");
|
|
1310
1282
|
state.selected = 0;
|
|
1283
|
+
state.slashOffset = 0;
|
|
1311
1284
|
}
|
|
1312
1285
|
|
|
1313
1286
|
function currentSlashMatches(state) {
|
|
1314
1287
|
if (!state.buffer.startsWith("/")) return [];
|
|
1315
|
-
return getSlashCommandMatches(state.buffer.slice(1))
|
|
1288
|
+
return getSlashCommandMatches(state.buffer.slice(1));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function getSlashVisibleLimit() {
|
|
1292
|
+
return 10;
|
|
1316
1293
|
}
|
|
1317
1294
|
|
|
1318
1295
|
function renderAgentInput(state) {
|
|
@@ -1320,20 +1297,25 @@ function renderAgentInput(state) {
|
|
|
1320
1297
|
const prompt = "> ";
|
|
1321
1298
|
const lines = state.buffer.split("\n");
|
|
1322
1299
|
const inputLines = [`${prompt}${lines[0] || ""}`, ...lines.slice(1).map((line) => ` ${line}`)];
|
|
1323
|
-
const cwdLine = colorMuted(` ${
|
|
1300
|
+
const cwdLine = colorMuted(` ${buildAgentStatusLine(state)}`);
|
|
1324
1301
|
const menuLines = [];
|
|
1325
1302
|
if (state.slashOpen) {
|
|
1326
1303
|
const matches = currentSlashMatches(state);
|
|
1327
1304
|
if (matches.length === 0) {
|
|
1328
1305
|
menuLines.push(" нет команд");
|
|
1329
1306
|
} else {
|
|
1330
|
-
|
|
1331
|
-
|
|
1307
|
+
const visibleLimit = getSlashVisibleLimit();
|
|
1308
|
+
const offset = Math.max(0, Math.min(state.slashOffset || 0, Math.max(0, matches.length - visibleLimit)));
|
|
1309
|
+
const visibleMatches = matches.slice(offset, offset + visibleLimit);
|
|
1310
|
+
for (let index = 0; index < visibleMatches.length; index += 1) {
|
|
1311
|
+
const absoluteIndex = offset + index;
|
|
1312
|
+
const selected = absoluteIndex === state.selected;
|
|
1332
1313
|
const marker = selected ? ">" : " ";
|
|
1333
|
-
const row = `${marker} ${
|
|
1314
|
+
const row = `${marker} ${visibleMatches[index].command.padEnd(24)} ${visibleMatches[index].description}`;
|
|
1334
1315
|
menuLines.push(selected ? colorSlashSelection(row) : ` ${row.slice(2)}`);
|
|
1335
1316
|
}
|
|
1336
|
-
|
|
1317
|
+
const shownTo = Math.min(offset + visibleLimit, matches.length);
|
|
1318
|
+
menuLines.push(` ↑/↓ выбрать • Enter выполнить • Esc закрыть • ${offset + 1}-${shownTo} из ${matches.length}`);
|
|
1337
1319
|
}
|
|
1338
1320
|
}
|
|
1339
1321
|
|
|
@@ -1447,6 +1429,35 @@ function printAgentHistory(history) {
|
|
|
1447
1429
|
}
|
|
1448
1430
|
}
|
|
1449
1431
|
|
|
1432
|
+
async function refreshAgentAiStatus(state) {
|
|
1433
|
+
try {
|
|
1434
|
+
const config = await loadConfig();
|
|
1435
|
+
const name = getActiveProfileName(config);
|
|
1436
|
+
const profile = config.ai.profiles?.[name] || {
|
|
1437
|
+
provider: config.ai.provider,
|
|
1438
|
+
model: config.ai.model,
|
|
1439
|
+
baseUrl: config.ai.baseUrl,
|
|
1440
|
+
};
|
|
1441
|
+
state.aiStatus = { name, provider: profile.provider || "-", model: profile.model || "-" };
|
|
1442
|
+
} catch {
|
|
1443
|
+
state.aiStatus = null;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function buildAgentStatusLine(state) {
|
|
1448
|
+
const cwd = process.cwd();
|
|
1449
|
+
const ai = state.aiStatus;
|
|
1450
|
+
if (!ai) return cwd;
|
|
1451
|
+
const kind = {
|
|
1452
|
+
ollama: "локальная",
|
|
1453
|
+
openai: "API",
|
|
1454
|
+
openrouter: "API",
|
|
1455
|
+
codex: "Codex",
|
|
1456
|
+
}[ai.provider] || ai.provider;
|
|
1457
|
+
const model = ai.model && ai.model !== "-" ? ` • ${ai.model}` : "";
|
|
1458
|
+
return `${cwd} | AI: ${kind}${model} (${ai.name})`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1450
1461
|
function compactAgentHistory(history) {
|
|
1451
1462
|
if (history.length <= 8) return history;
|
|
1452
1463
|
const summary = history.slice(0, -6)
|
|
@@ -2245,185 +2256,6 @@ async function handleSettings(args) {
|
|
|
2245
2256
|
throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
|
|
2246
2257
|
}
|
|
2247
2258
|
|
|
2248
|
-
async function handleGosuslugi(args) {
|
|
2249
|
-
const [action = "status", ...rest] = args;
|
|
2250
|
-
const options = parseOptions(rest);
|
|
2251
|
-
|
|
2252
|
-
if (action === "terms") {
|
|
2253
|
-
console.log(GOSUSLUGI_CONSENT_TEXT);
|
|
2254
|
-
return;
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
if (action === "consent") {
|
|
2258
|
-
await acceptGosuslugiConsent(options);
|
|
2259
|
-
return;
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
if (action === "status") {
|
|
2263
|
-
const config = await loadConfig();
|
|
2264
|
-
const secrets = await loadSecrets();
|
|
2265
|
-
const tokens = secrets.gosuslugi?.tokens || null;
|
|
2266
|
-
const browserSession = secrets.gosuslugiBrowser || null;
|
|
2267
|
-
const consent = secrets.gosuslugiConsent || null;
|
|
2268
|
-
printKeyValue({
|
|
2269
|
-
mode: config.gosuslugi?.mode || "personal-browser",
|
|
2270
|
-
enabled: config.gosuslugi?.enabled ? "yes" : "no",
|
|
2271
|
-
browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
2272
|
-
browserProfileExists: existsSync(GOSUSLUGI_BROWSER_PROFILE_DIR) ? "yes" : "no",
|
|
2273
|
-
browserConnected: browserSession?.connectedAt ? "yes" : "unknown",
|
|
2274
|
-
browserConnectedAt: browserSession?.connectedAt || "-",
|
|
2275
|
-
oauthConfigured: isGosuslugiConfigured(config) ? "yes" : "no",
|
|
2276
|
-
consent: consent?.version === GOSUSLUGI_CONSENT_VERSION ? "accepted" : "not accepted",
|
|
2277
|
-
consentAt: consent?.acceptedAt || "-",
|
|
2278
|
-
clientId: config.gosuslugi?.clientId ? maskSecret(config.gosuslugi.clientId) : "-",
|
|
2279
|
-
authUrl: config.gosuslugi?.authUrl || "-",
|
|
2280
|
-
tokenUrl: config.gosuslugi?.tokenUrl || "-",
|
|
2281
|
-
userinfoUrl: config.gosuslugi?.userinfoUrl || "-",
|
|
2282
|
-
redirectUri: gosuslugiRedirectUri(config),
|
|
2283
|
-
connected: tokens?.access_token ? "yes" : "no",
|
|
2284
|
-
savedAt: secrets.gosuslugi?.savedAt || "-",
|
|
2285
|
-
expiresAt: secrets.gosuslugi?.expiresAt || "-",
|
|
2286
|
-
});
|
|
2287
|
-
return;
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
if (action === "check") {
|
|
2291
|
-
const result = await gosuslugiCheck(options);
|
|
2292
|
-
if (options.json) printJson(result);
|
|
2293
|
-
else printKeyValue(result);
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
if (action === "keepalive") {
|
|
2298
|
-
await gosuslugiKeepalive(options);
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
if (action === "install-keepalive") {
|
|
2303
|
-
await installGosuslugiKeepaliveTask(options);
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
if (action === "uninstall-keepalive") {
|
|
2308
|
-
await uninstallGosuslugiKeepaliveTask(options);
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
if (action === "keepalive-status") {
|
|
2313
|
-
await printGosuslugiKeepaliveTaskStatus(options);
|
|
2314
|
-
return;
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
if (action === "connect") {
|
|
2318
|
-
await gosuslugiBrowserConnect(options);
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
if (action === "open") {
|
|
2323
|
-
await gosuslugiBrowserOpen(targetOrDefault(rest, options), options);
|
|
2324
|
-
return;
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
if (action === "text") {
|
|
2328
|
-
const result = await gosuslugiBrowserReadText(targetOrDefault(rest, options), options);
|
|
2329
|
-
if (options.output) {
|
|
2330
|
-
await writeFile(path.resolve(options.output), result, "utf8");
|
|
2331
|
-
console.log(`Файл сохранен: ${path.resolve(options.output)}`);
|
|
2332
|
-
} else {
|
|
2333
|
-
console.log(result);
|
|
2334
|
-
}
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
if (action === "screenshot") {
|
|
2339
|
-
const outputFile = path.resolve(options.output || "gosuslugi-page.png");
|
|
2340
|
-
await gosuslugiBrowserScreenshot(targetOrDefault(rest, options), outputFile, options);
|
|
2341
|
-
saveArtifact("gosuslugi-screenshot", targetOrDefault(rest, options), outputFile, { url: targetOrDefault(rest, options) });
|
|
2342
|
-
console.log(`Файл сохранен: ${outputFile}`);
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
if (action === "whoami" || action === "profile") {
|
|
2347
|
-
const result = await gosuslugiWhoami(options);
|
|
2348
|
-
if (options.json) printJson(result);
|
|
2349
|
-
else printKeyValue(result.summary);
|
|
2350
|
-
return;
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
if (action === "debt" || action === "debts" || action === "payments") {
|
|
2354
|
-
const result = await gosuslugiDebt(options);
|
|
2355
|
-
if (options.json) printJson(result);
|
|
2356
|
-
else printGosuslugiDebt(result);
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
if (action === "notifications" || action === "notices") {
|
|
2361
|
-
const result = await gosuslugiNotifications(options);
|
|
2362
|
-
if (options.json) printJson(result);
|
|
2363
|
-
else printGosuslugiNotifications(result);
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
if (action === "mark-read") {
|
|
2368
|
-
await gosuslugiMarkNotificationsRead(options);
|
|
2369
|
-
return;
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
if (action === "configure") {
|
|
2373
|
-
const current = await loadConfig();
|
|
2374
|
-
const next = {
|
|
2375
|
-
...(current.gosuslugi || {}),
|
|
2376
|
-
enabled: true,
|
|
2377
|
-
mode: "personal-local",
|
|
2378
|
-
authUrl: options["auth-url"] || current.gosuslugi?.authUrl || "",
|
|
2379
|
-
tokenUrl: options["token-url"] || current.gosuslugi?.tokenUrl || "",
|
|
2380
|
-
userinfoUrl: options["userinfo-url"] || current.gosuslugi?.userinfoUrl || "",
|
|
2381
|
-
clientId: options["client-id"] || current.gosuslugi?.clientId || "",
|
|
2382
|
-
clientSecret: options["client-secret"] || current.gosuslugi?.clientSecret || "",
|
|
2383
|
-
scope: options.scope || current.gosuslugi?.scope || "openid",
|
|
2384
|
-
redirectHost: options["redirect-host"] || current.gosuslugi?.redirectHost || "127.0.0.1",
|
|
2385
|
-
redirectPort: Number(options["redirect-port"] || current.gosuslugi?.redirectPort || 18791),
|
|
2386
|
-
redirectPath: options["redirect-path"] || current.gosuslugi?.redirectPath || "/gosuslugi/callback",
|
|
2387
|
-
};
|
|
2388
|
-
await saveConfig({ gosuslugi: next });
|
|
2389
|
-
console.log("Настройки личного локального подключения Госуслуг сохранены.");
|
|
2390
|
-
console.log(`Redirect URI: ${gosuslugiRedirectUri({ gosuslugi: next })}`);
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
if (action === "login") {
|
|
2395
|
-
const result = await gosuslugiLogin(options);
|
|
2396
|
-
printKeyValue(result);
|
|
2397
|
-
return;
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
if (action === "logout") {
|
|
2401
|
-
const secrets = await loadSecrets();
|
|
2402
|
-
delete secrets.gosuslugi;
|
|
2403
|
-
delete secrets.gosuslugiBrowser;
|
|
2404
|
-
await saveSecrets(secrets);
|
|
2405
|
-
if (options.profile || options.all) {
|
|
2406
|
-
await rm(GOSUSLUGI_BROWSER_PROFILE_DIR, { recursive: true, force: true }).catch(() => {});
|
|
2407
|
-
console.log("Локальный браузерный профиль Госуслуг удален.");
|
|
2408
|
-
}
|
|
2409
|
-
console.log("Локальное подключение Госуслуг удалено.");
|
|
2410
|
-
return;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
if (action === "userinfo" || action === "me") {
|
|
2414
|
-
const result = await gosuslugiUserinfo(options);
|
|
2415
|
-
if (options.json) printJson(result);
|
|
2416
|
-
else printKeyValue(flattenObjectForPrint(result));
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
throw new Error("Команды gosuslugi: terms, consent, status, check, keepalive, install-keepalive, keepalive-status, uninstall-keepalive, connect, open, text, screenshot, whoami, debt, notifications, mark-read, logout, configure, login, userinfo.");
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
function targetOrDefault(args, options = {}) {
|
|
2424
|
-
return options.url || args.find((item) => !item.startsWith("--")) || GOSUSLUGI_DEFAULT_URL;
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
2259
|
async function handleWiki(args) {
|
|
2428
2260
|
const [action = "links"] = args;
|
|
2429
2261
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -2439,7 +2271,6 @@ async function handleWiki(args) {
|
|
|
2439
2271
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
2440
2272
|
["Платформа агента", `${base}/Платформа-агента`],
|
|
2441
2273
|
["Браузерный агент", `${base}/Браузерный-агент`],
|
|
2442
|
-
["Подключение Госуслуг", `${base}/Подключение-Госуслуг`],
|
|
2443
2274
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
2444
2275
|
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
2445
2276
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
@@ -3376,189 +3207,6 @@ async function openUrl(url) {
|
|
|
3376
3207
|
await runCommand("xdg-open", [url], { inherit: false });
|
|
3377
3208
|
}
|
|
3378
3209
|
|
|
3379
|
-
async function gosuslugiLogin(options = {}) {
|
|
3380
|
-
const config = await loadConfig();
|
|
3381
|
-
if (!isGosuslugiConfigured(config)) {
|
|
3382
|
-
throw new Error("Личное подключение не настроено. Пример: iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid");
|
|
3383
|
-
}
|
|
3384
|
-
await ensureGosuslugiConsent(options);
|
|
3385
|
-
|
|
3386
|
-
const state = randomUrlSafe(24);
|
|
3387
|
-
const codeVerifier = randomUrlSafe(64);
|
|
3388
|
-
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
3389
|
-
const redirectUri = gosuslugiRedirectUri(config);
|
|
3390
|
-
const callback = waitForOAuthCallback(config.gosuslugi, state, Number(options.timeout || 180000));
|
|
3391
|
-
const authUrl = new URL(config.gosuslugi.authUrl);
|
|
3392
|
-
authUrl.searchParams.set("response_type", "code");
|
|
3393
|
-
authUrl.searchParams.set("client_id", config.gosuslugi.clientId);
|
|
3394
|
-
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
3395
|
-
authUrl.searchParams.set("scope", config.gosuslugi.scope || "openid");
|
|
3396
|
-
authUrl.searchParams.set("state", state);
|
|
3397
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
3398
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
3399
|
-
|
|
3400
|
-
console.log("Открываю экран входа Госуслуг в браузере для личного локального подключения.");
|
|
3401
|
-
console.log("После входа CLI примет callback на локальном адресе и сохранит данные доступа только на этом компьютере.");
|
|
3402
|
-
await openUrl(authUrl.toString());
|
|
3403
|
-
const params = await callback;
|
|
3404
|
-
if (params.error) throw new Error(`Госуслуги вернули ошибку: ${params.error} ${params.error_description || ""}`.trim());
|
|
3405
|
-
if (!params.code) throw new Error("Authorization code не получен.");
|
|
3406
|
-
|
|
3407
|
-
const tokens = await exchangeGosuslugiCode(config, {
|
|
3408
|
-
code: params.code,
|
|
3409
|
-
codeVerifier,
|
|
3410
|
-
redirectUri,
|
|
3411
|
-
});
|
|
3412
|
-
const secrets = await loadSecrets();
|
|
3413
|
-
const now = new Date();
|
|
3414
|
-
const expiresAt = tokens.expires_in ? new Date(now.getTime() + Number(tokens.expires_in) * 1000).toISOString() : "";
|
|
3415
|
-
secrets.gosuslugi = {
|
|
3416
|
-
savedAt: now.toISOString(),
|
|
3417
|
-
expiresAt,
|
|
3418
|
-
tokens,
|
|
3419
|
-
};
|
|
3420
|
-
await saveSecrets(secrets);
|
|
3421
|
-
return {
|
|
3422
|
-
connected: "yes",
|
|
3423
|
-
savedAt: secrets.gosuslugi.savedAt,
|
|
3424
|
-
expiresAt: expiresAt || "-",
|
|
3425
|
-
tokenType: tokens.token_type || "-",
|
|
3426
|
-
scope: tokens.scope || config.gosuslugi.scope || "-",
|
|
3427
|
-
};
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
async function acceptGosuslugiConsent(options = {}) {
|
|
3431
|
-
console.log(GOSUSLUGI_CONSENT_TEXT);
|
|
3432
|
-
if (!options.yes) {
|
|
3433
|
-
const accepted = await confirm("Да, подключить личные Госуслуги к локальному iola-cli? [y/N] ");
|
|
3434
|
-
if (!accepted) {
|
|
3435
|
-
throw new Error("Подключение Госуслуг отменено пользователем.");
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
const secrets = await loadSecrets();
|
|
3439
|
-
secrets.gosuslugiConsent = {
|
|
3440
|
-
version: GOSUSLUGI_CONSENT_VERSION,
|
|
3441
|
-
acceptedAt: new Date().toISOString(),
|
|
3442
|
-
user: os.userInfo().username,
|
|
3443
|
-
host: os.hostname(),
|
|
3444
|
-
};
|
|
3445
|
-
await saveSecrets(secrets);
|
|
3446
|
-
console.log("Согласие сохранено локально.");
|
|
3447
|
-
}
|
|
3448
|
-
|
|
3449
|
-
async function ensureGosuslugiConsent(options = {}) {
|
|
3450
|
-
const secrets = await loadSecrets();
|
|
3451
|
-
if (secrets.gosuslugiConsent?.version === GOSUSLUGI_CONSENT_VERSION) return;
|
|
3452
|
-
await acceptGosuslugiConsent(options);
|
|
3453
|
-
}
|
|
3454
|
-
|
|
3455
|
-
async function requireGosuslugiConsent() {
|
|
3456
|
-
await ensureGosuslugiConsent();
|
|
3457
|
-
}
|
|
3458
|
-
|
|
3459
|
-
function waitForOAuthCallback(settings, expectedState, timeoutMs) {
|
|
3460
|
-
const host = settings.redirectHost || "127.0.0.1";
|
|
3461
|
-
const port = Number(settings.redirectPort || 18791);
|
|
3462
|
-
const callbackPath = settings.redirectPath || "/gosuslugi/callback";
|
|
3463
|
-
return new Promise((resolve, reject) => {
|
|
3464
|
-
const timer = setTimeout(() => {
|
|
3465
|
-
server.close(() => {});
|
|
3466
|
-
reject(new Error("Истекло время ожидания входа через Госуслуги."));
|
|
3467
|
-
}, timeoutMs);
|
|
3468
|
-
const server = createServer((req, res) => {
|
|
3469
|
-
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
3470
|
-
if (url.pathname !== callbackPath) {
|
|
3471
|
-
res.statusCode = 404;
|
|
3472
|
-
res.end("Not found");
|
|
3473
|
-
return;
|
|
3474
|
-
}
|
|
3475
|
-
const params = Object.fromEntries(url.searchParams.entries());
|
|
3476
|
-
if (params.state !== expectedState) {
|
|
3477
|
-
res.statusCode = 400;
|
|
3478
|
-
res.end("Invalid state");
|
|
3479
|
-
clearTimeout(timer);
|
|
3480
|
-
server.close(() => {});
|
|
3481
|
-
reject(new Error("OAuth state не совпал. Вход отменен."));
|
|
3482
|
-
return;
|
|
3483
|
-
}
|
|
3484
|
-
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
3485
|
-
res.end("<!doctype html><meta charset=\"utf-8\"><title>iola</title><body>Вход выполнен. Можно закрыть это окно и вернуться в терминал.</body>");
|
|
3486
|
-
clearTimeout(timer);
|
|
3487
|
-
server.close(() => resolve(params));
|
|
3488
|
-
});
|
|
3489
|
-
server.once("error", (error) => {
|
|
3490
|
-
clearTimeout(timer);
|
|
3491
|
-
reject(error);
|
|
3492
|
-
});
|
|
3493
|
-
server.listen(port, host);
|
|
3494
|
-
});
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3497
|
-
async function exchangeGosuslugiCode(config, { code, codeVerifier, redirectUri }) {
|
|
3498
|
-
const body = new URLSearchParams();
|
|
3499
|
-
body.set("grant_type", "authorization_code");
|
|
3500
|
-
body.set("code", code);
|
|
3501
|
-
body.set("redirect_uri", redirectUri);
|
|
3502
|
-
body.set("client_id", config.gosuslugi.clientId);
|
|
3503
|
-
body.set("code_verifier", codeVerifier);
|
|
3504
|
-
body.set("client_mode", config.gosuslugi.mode || "personal-local");
|
|
3505
|
-
if (config.gosuslugi.clientSecret) body.set("client_secret", config.gosuslugi.clientSecret);
|
|
3506
|
-
|
|
3507
|
-
const response = await fetch(config.gosuslugi.tokenUrl, {
|
|
3508
|
-
method: "POST",
|
|
3509
|
-
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
3510
|
-
body,
|
|
3511
|
-
});
|
|
3512
|
-
const text = await response.text();
|
|
3513
|
-
let payload = {};
|
|
3514
|
-
try {
|
|
3515
|
-
payload = text ? JSON.parse(text) : {};
|
|
3516
|
-
} catch {
|
|
3517
|
-
payload = { raw: text };
|
|
3518
|
-
}
|
|
3519
|
-
if (!response.ok) {
|
|
3520
|
-
throw new Error(`Token endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
3521
|
-
}
|
|
3522
|
-
return payload;
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
async function gosuslugiUserinfo() {
|
|
3526
|
-
const config = await loadConfig();
|
|
3527
|
-
const secrets = await loadSecrets();
|
|
3528
|
-
const accessToken = secrets.gosuslugi?.tokens?.access_token;
|
|
3529
|
-
if (!accessToken) throw new Error("Госуслуги не подключены. Запустите: iola gosuslugi login");
|
|
3530
|
-
if (!config.gosuslugi?.userinfoUrl) throw new Error("userinfoUrl не настроен.");
|
|
3531
|
-
const response = await fetch(config.gosuslugi.userinfoUrl, {
|
|
3532
|
-
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
|
3533
|
-
});
|
|
3534
|
-
const text = await response.text();
|
|
3535
|
-
let payload = {};
|
|
3536
|
-
try {
|
|
3537
|
-
payload = text ? JSON.parse(text) : {};
|
|
3538
|
-
} catch {
|
|
3539
|
-
payload = { raw: text };
|
|
3540
|
-
}
|
|
3541
|
-
if (!response.ok) throw new Error(`Userinfo endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
3542
|
-
return payload;
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
function isGosuslugiConfigured(config) {
|
|
3546
|
-
return Boolean(config.gosuslugi?.authUrl && config.gosuslugi?.tokenUrl && config.gosuslugi?.clientId);
|
|
3547
|
-
}
|
|
3548
|
-
|
|
3549
|
-
function gosuslugiRedirectUri(config) {
|
|
3550
|
-
const settings = config.gosuslugi || DEFAULT_AI_CONFIG.gosuslugi;
|
|
3551
|
-
return `http://${settings.redirectHost || "127.0.0.1"}:${Number(settings.redirectPort || 18791)}${settings.redirectPath || "/gosuslugi/callback"}`;
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
function randomUrlSafe(bytes) {
|
|
3555
|
-
return base64Url(randomBytes(bytes));
|
|
3556
|
-
}
|
|
3557
|
-
|
|
3558
|
-
function base64Url(buffer) {
|
|
3559
|
-
return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3560
|
-
}
|
|
3561
|
-
|
|
3562
3210
|
function maskSecret(value) {
|
|
3563
3211
|
const text = String(value || "");
|
|
3564
3212
|
if (text.length <= 8) return text ? "***" : "-";
|
|
@@ -4677,6 +4325,171 @@ async function useAiProvider(args) {
|
|
|
4677
4325
|
console.log(`AI-провайдер переключен: ${provider}, профиль: ${profileName}, модель: ${defaultModel}`);
|
|
4678
4326
|
}
|
|
4679
4327
|
|
|
4328
|
+
async function slashModelMenu(args = []) {
|
|
4329
|
+
const [target, maybeModel] = args;
|
|
4330
|
+
const normalizedTarget = normalizeModelMenuTarget(target);
|
|
4331
|
+
|
|
4332
|
+
if (normalizedTarget && maybeModel) {
|
|
4333
|
+
const directTarget = normalizedTarget === "api" ? await getDefaultApiProviderForModelSwitch() : normalizedTarget;
|
|
4334
|
+
await switchModelTarget(directTarget, maybeModel);
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
const selectedTarget = normalizedTarget || await chooseModelTarget();
|
|
4339
|
+
if (!selectedTarget) return;
|
|
4340
|
+
|
|
4341
|
+
await openModelTargetMenu(selectedTarget);
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
function normalizeModelMenuTarget(value = "") {
|
|
4345
|
+
const normalized = String(value || "").trim().toLocaleLowerCase("ru-RU");
|
|
4346
|
+
if (!normalized) return "";
|
|
4347
|
+
if (["local", "локальная", "локально", "ollama"].includes(normalized)) return "local";
|
|
4348
|
+
if (["api", "апи"].includes(normalized)) return "api";
|
|
4349
|
+
if (normalized === "openai") return "openai";
|
|
4350
|
+
if (normalized === "openrouter" || normalized === "router") return "openrouter";
|
|
4351
|
+
if (["codex", "кодекс"].includes(normalized)) return "codex";
|
|
4352
|
+
return "";
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
async function chooseModelTarget() {
|
|
4356
|
+
console.log("Выберите AI-подключение:");
|
|
4357
|
+
console.log(" 1. Локальная модель (Ollama)");
|
|
4358
|
+
console.log(" 2. API (OpenAI/OpenRouter)");
|
|
4359
|
+
console.log(" 3. Codex CLI");
|
|
4360
|
+
console.log(" 0. Отмена");
|
|
4361
|
+
|
|
4362
|
+
const answer = await askText("Номер: ");
|
|
4363
|
+
return { 1: "local", 2: "api", 3: "codex" }[answer.trim()] || "";
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
async function openModelTargetMenu(target) {
|
|
4367
|
+
if (target === "local") {
|
|
4368
|
+
const model = await chooseAiModel("ollama");
|
|
4369
|
+
if (model) await switchModelTarget("local", model);
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
if (target === "codex") {
|
|
4374
|
+
const model = await chooseAiModel("codex");
|
|
4375
|
+
if (model) await switchModelTarget("codex", model);
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
if (target === "openai" || target === "openrouter") {
|
|
4380
|
+
const model = await chooseAiModel(target);
|
|
4381
|
+
if (model) await switchModelTarget(target, model);
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
const provider = await chooseApiProvider();
|
|
4386
|
+
if (!provider) return;
|
|
4387
|
+
const model = await chooseAiModel(provider);
|
|
4388
|
+
if (model) await switchModelTarget(provider, model);
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
async function chooseApiProvider() {
|
|
4392
|
+
const config = await loadConfig();
|
|
4393
|
+
const apiProfiles = Object.entries(config.ai.profiles || {})
|
|
4394
|
+
.filter(([, profile]) => profile.provider === "openai" || profile.provider === "openrouter")
|
|
4395
|
+
.map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})` }));
|
|
4396
|
+
const choices = [
|
|
4397
|
+
...apiProfiles,
|
|
4398
|
+
{ id: "openai", label: "OpenAI API" },
|
|
4399
|
+
{ id: "openrouter", label: "OpenRouter API" },
|
|
4400
|
+
].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index);
|
|
4401
|
+
|
|
4402
|
+
console.log("Выберите API-подключение:");
|
|
4403
|
+
choices.forEach((item, index) => console.log(` ${index + 1}. ${item.label}`));
|
|
4404
|
+
console.log(" 0. Отмена");
|
|
4405
|
+
|
|
4406
|
+
const answer = Number(await askText("Номер: "));
|
|
4407
|
+
return choices[answer - 1]?.id || "";
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
async function getDefaultApiProviderForModelSwitch() {
|
|
4411
|
+
const config = await loadConfig();
|
|
4412
|
+
const activeProfile = config.ai.profiles?.[getActiveProfileName(config)];
|
|
4413
|
+
if (activeProfile?.provider === "openai" || activeProfile?.provider === "openrouter") return activeProfile.provider;
|
|
4414
|
+
const apiProfile = Object.values(config.ai.profiles || {}).find((profile) => profile.provider === "openai" || profile.provider === "openrouter");
|
|
4415
|
+
return apiProfile?.provider || "openai";
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
async function chooseAiModel(provider) {
|
|
4419
|
+
let search = "";
|
|
4420
|
+
if (provider === "openrouter" || provider === "openai") {
|
|
4421
|
+
search = (await askText("Фильтр моделей (Enter - без фильтра): ")).trim();
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
let models;
|
|
4425
|
+
try {
|
|
4426
|
+
models = await listAiModels(provider);
|
|
4427
|
+
} catch (error) {
|
|
4428
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4429
|
+
return "";
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
let filtered = search
|
|
4433
|
+
? models.filter((model) => model.id.toLocaleLowerCase("ru-RU").includes(search.toLocaleLowerCase("ru-RU")))
|
|
4434
|
+
: models;
|
|
4435
|
+
|
|
4436
|
+
if (filtered.length === 0) {
|
|
4437
|
+
console.log("Модели не найдены.");
|
|
4438
|
+
return "";
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
const limit = 25;
|
|
4442
|
+
if (filtered.length > limit) {
|
|
4443
|
+
filtered = filtered.slice(0, limit);
|
|
4444
|
+
console.log(`Показаны первые ${limit} моделей. Для точного выбора запустите /model и задайте фильтр.`);
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
console.log("Выберите модель:");
|
|
4448
|
+
filtered.forEach((model, index) => console.log(` ${index + 1}. ${model.id}${model.note ? ` - ${model.note}` : ""}`));
|
|
4449
|
+
console.log(" 0. Отмена");
|
|
4450
|
+
|
|
4451
|
+
const answer = Number(await askText("Номер: "));
|
|
4452
|
+
return filtered[answer - 1]?.id || "";
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
async function switchModelTarget(target, model) {
|
|
4456
|
+
const config = await loadConfig();
|
|
4457
|
+
const provider = target === "local" ? "ollama" : target;
|
|
4458
|
+
const profileName = provider === "ollama" ? "local" : provider;
|
|
4459
|
+
const currentProfile = config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model });
|
|
4460
|
+
const profile = {
|
|
4461
|
+
...currentProfile,
|
|
4462
|
+
provider,
|
|
4463
|
+
model,
|
|
4464
|
+
};
|
|
4465
|
+
|
|
4466
|
+
await saveConfig({
|
|
4467
|
+
ai: {
|
|
4468
|
+
...config.ai,
|
|
4469
|
+
activeProfile: profileName,
|
|
4470
|
+
provider,
|
|
4471
|
+
model,
|
|
4472
|
+
baseUrl: profile.baseUrl || config.ai.baseUrl,
|
|
4473
|
+
profiles: {
|
|
4474
|
+
...(config.ai.profiles || {}),
|
|
4475
|
+
[profileName]: profile,
|
|
4476
|
+
},
|
|
4477
|
+
},
|
|
4478
|
+
});
|
|
4479
|
+
|
|
4480
|
+
console.log(`Активная модель: ${profileName} (${provider}, ${model})`);
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
async function askText(question) {
|
|
4484
|
+
if (!process.stdin.isTTY) return "";
|
|
4485
|
+
const rl = readline.createInterface({ input, output });
|
|
4486
|
+
try {
|
|
4487
|
+
return await rl.question(question);
|
|
4488
|
+
} finally {
|
|
4489
|
+
rl.close();
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4680
4493
|
async function aiContext(args) {
|
|
4681
4494
|
const options = parseOptions(args);
|
|
4682
4495
|
const query = options._.join(" ").trim();
|
|
@@ -6075,12 +5888,6 @@ async function aiAsk(args, context = {}) {
|
|
|
6075
5888
|
throw new Error('Текст вопроса обязателен. Пример: iola ai ask "Какие школы есть на улице Петрова?"');
|
|
6076
5889
|
}
|
|
6077
5890
|
|
|
6078
|
-
if (!options.bare && isGosuslugiPersonalIntent(question)) {
|
|
6079
|
-
const answer = await answerGosuslugiQuestion(question, options);
|
|
6080
|
-
if (!options.quiet) console.log(answer);
|
|
6081
|
-
return answer;
|
|
6082
|
-
}
|
|
6083
|
-
|
|
6084
5891
|
const config = await loadConfig();
|
|
6085
5892
|
const providerConfig = await resolveUsableAiProfile(config, options);
|
|
6086
5893
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
@@ -6249,7 +6056,7 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
6249
6056
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
6250
6057
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
6251
6058
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
6252
|
-
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}
|
|
6059
|
+
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
6253
6060
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
6254
6061
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
6255
6062
|
`Вопрос: ${question}`,
|
|
@@ -6279,12 +6086,6 @@ function inferToolPlan(question, options = {}) {
|
|
|
6279
6086
|
const steps = [];
|
|
6280
6087
|
if (normalized.includes("без телефона")) {
|
|
6281
6088
|
steps.push({ tool: "export_report", args: { name: "missing-phones" } });
|
|
6282
|
-
} else if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
|
|
6283
|
-
steps.push({ tool: "gosuslugi_notifications", args: { unread: /непрочитан|нов/iu.test(normalized), limit: 15 } });
|
|
6284
|
-
} else if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
|
|
6285
|
-
steps.push({ tool: "gosuslugi_debt", args: {} });
|
|
6286
|
-
} else if (/(фио|дата рождения|профиль|кто я)/iu.test(normalized) && /госуслуг/iu.test(normalized)) {
|
|
6287
|
-
steps.push({ tool: "gosuslugi_whoami", args: {} });
|
|
6288
6089
|
} else {
|
|
6289
6090
|
const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
|
|
6290
6091
|
steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
|
|
@@ -6374,18 +6175,6 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6374
6175
|
const text = await runBrowserAutomation("text", { url: step.args?.url, waitMs: Number(step.args?.waitMs || 0), timeout: Number(step.args?.timeout || 30000), viewport: step.args?.viewport || "1366x768" });
|
|
6375
6176
|
current = [{ url: step.args?.url, text }];
|
|
6376
6177
|
outputs.push({ tool: step.tool, rows: 1 });
|
|
6377
|
-
} else if (step.tool === "gosuslugi_whoami") {
|
|
6378
|
-
const result = await gosuslugiWhoami(step.args || {});
|
|
6379
|
-
current = [result.summary];
|
|
6380
|
-
outputs.push({ tool: step.tool, rows: 1 });
|
|
6381
|
-
} else if (step.tool === "gosuslugi_debt") {
|
|
6382
|
-
const result = await gosuslugiDebt(step.args || {});
|
|
6383
|
-
current = [{ total: result.total, amount: result.amount, debts: result.debts }];
|
|
6384
|
-
outputs.push({ tool: step.tool, rows: result.debts.length });
|
|
6385
|
-
} else if (step.tool === "gosuslugi_notifications") {
|
|
6386
|
-
const result = await gosuslugiNotifications(step.args || {});
|
|
6387
|
-
current = [{ total: result.total, unread: result.unread, items: result.items }];
|
|
6388
|
-
outputs.push({ tool: step.tool, rows: result.items.length });
|
|
6389
6178
|
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
6390
6179
|
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
6391
6180
|
current = Array.isArray(result) ? result : [result];
|
|
@@ -7122,17 +6911,6 @@ async function onboard(args = []) {
|
|
|
7122
6911
|
if (status.installed === "yes") console.log("Browser runtime уже установлен.");
|
|
7123
6912
|
else await installBrowserRuntime();
|
|
7124
6913
|
}
|
|
7125
|
-
if (components.includes("gosuslugi")) {
|
|
7126
|
-
if (process.stdin.isTTY) await handleGosuslugi(["consent"]);
|
|
7127
|
-
else await handleGosuslugi(["terms"]);
|
|
7128
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7129
|
-
if (process.stdin.isTTY && await confirm("Открыть Госуслуги для входа сейчас? [Y/n] ")) {
|
|
7130
|
-
await gosuslugiBrowserConnect({ yes: true });
|
|
7131
|
-
await installGosuslugiKeepaliveTask({ interval: "30m" });
|
|
7132
|
-
} else {
|
|
7133
|
-
console.log("Подключить личные Госуслуги позже: iola gosuslugi connect");
|
|
7134
|
-
}
|
|
7135
|
-
}
|
|
7136
6914
|
if (components.includes("index")) {
|
|
7137
6915
|
await setFilesMode("read-only", await loadConfig());
|
|
7138
6916
|
console.log("Индекс документов можно запустить командой: iola index folder ./docs");
|
|
@@ -7166,7 +6944,6 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7166
6944
|
8: "archive",
|
|
7167
6945
|
9: "index",
|
|
7168
6946
|
10: "browser",
|
|
7169
|
-
11: "gosuslugi",
|
|
7170
6947
|
};
|
|
7171
6948
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
7172
6949
|
} finally {
|
|
@@ -7175,18 +6952,16 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7175
6952
|
}
|
|
7176
6953
|
|
|
7177
6954
|
async function getOnboardComponentStatus() {
|
|
7178
|
-
const [config, readiness, browser, archive, codexVersion, ollamaVersion
|
|
6955
|
+
const [config, readiness, browser, archive, codexVersion, ollamaVersion] = await Promise.all([
|
|
7179
6956
|
loadConfig(),
|
|
7180
6957
|
getAiReadiness(),
|
|
7181
6958
|
getBrowserStatus(),
|
|
7182
6959
|
findCommand(["7z", "7zz", "7za"], ["--help"]).catch(() => null),
|
|
7183
6960
|
getCommandVersion("codex", ["--version"]),
|
|
7184
6961
|
getOllamaVersion(),
|
|
7185
|
-
loadSecrets(),
|
|
7186
6962
|
]);
|
|
7187
6963
|
const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
|
|
7188
6964
|
const policyReady = (config.toolsets?.enabled || []).includes("analyst");
|
|
7189
|
-
const gosuslugiReady = Boolean(config.gosuslugi?.enabled && existsSync(GOSUSLUGI_BROWSER_PROFILE_DIR) && secrets.gosuslugiBrowser?.connectedAt);
|
|
7190
6965
|
return {
|
|
7191
6966
|
workspace: workspaceReady,
|
|
7192
6967
|
policy: policyReady,
|
|
@@ -7198,7 +6973,6 @@ async function getOnboardComponentStatus() {
|
|
|
7198
6973
|
archive: Boolean(archive),
|
|
7199
6974
|
index: false,
|
|
7200
6975
|
browser: browser.installed === "yes",
|
|
7201
|
-
gosuslugi: gosuslugiReady,
|
|
7202
6976
|
};
|
|
7203
6977
|
}
|
|
7204
6978
|
|
|
@@ -7214,7 +6988,6 @@ function onboardComponentRows(status) {
|
|
|
7214
6988
|
["8", "archive", "7-Zip / архивы", "архиватор найден"],
|
|
7215
6989
|
["9", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
|
|
7216
6990
|
["10", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
7217
|
-
["11", "gosuslugi", "Личное подключение Госуслуг", "профиль и keepalive"],
|
|
7218
6991
|
];
|
|
7219
6992
|
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
7220
6993
|
}
|
|
@@ -7228,7 +7001,7 @@ function defaultOnboardSelection(status) {
|
|
|
7228
7001
|
}
|
|
7229
7002
|
|
|
7230
7003
|
function defaultOnboardComponents(status) {
|
|
7231
|
-
const map = { 1: "workspace", 2: "policy", 3: "ollama", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser"
|
|
7004
|
+
const map = { 1: "workspace", 2: "policy", 3: "ollama", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser" };
|
|
7232
7005
|
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
7233
7006
|
}
|
|
7234
7007
|
|
|
@@ -7478,8 +7251,7 @@ async function buildSkillsText(config, question = "", options = {}) {
|
|
|
7478
7251
|
const chunks = [];
|
|
7479
7252
|
const selected = selectSkillsForPrompt(config, question, options);
|
|
7480
7253
|
for (const skill of listSkills(config)) {
|
|
7481
|
-
|
|
7482
|
-
if (!active || !selected.has(skill.name)) continue;
|
|
7254
|
+
if (!skill.enabled || !selected.has(skill.name)) continue;
|
|
7483
7255
|
const text = await readFile(skill.file, "utf8");
|
|
7484
7256
|
chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
|
|
7485
7257
|
}
|
|
@@ -7495,7 +7267,6 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
7495
7267
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
7496
7268
|
if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
7497
7269
|
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
7498
|
-
if (enabled.has("gosuslugi") && /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized)) selected.add("gosuslugi");
|
|
7499
7270
|
return selected;
|
|
7500
7271
|
}
|
|
7501
7272
|
|
|
@@ -7719,534 +7490,6 @@ async function runBrowserAutomation(action, params) {
|
|
|
7719
7490
|
}
|
|
7720
7491
|
}
|
|
7721
7492
|
|
|
7722
|
-
async function ensureBrowserRuntimeForGosuslugi() {
|
|
7723
|
-
if (existsSync(BROWSER_RUNTIME_PACKAGE)) return;
|
|
7724
|
-
console.log("Browser runtime не установлен. Устанавливаю Playwright/Chromium для локального браузерного профиля.");
|
|
7725
|
-
await installBrowserRuntime();
|
|
7726
|
-
}
|
|
7727
|
-
|
|
7728
|
-
async function gosuslugiBrowserConnect(options = {}) {
|
|
7729
|
-
await ensureGosuslugiConsent({ yes: options.yes });
|
|
7730
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7731
|
-
await saveConfig({ gosuslugi: { ...(await loadConfig()).gosuslugi, enabled: true, mode: "personal-browser" } });
|
|
7732
|
-
const url = options.url || GOSUSLUGI_DEFAULT_URL;
|
|
7733
|
-
console.log(`Открываю Госуслуги в отдельном локальном профиле: ${GOSUSLUGI_BROWSER_PROFILE_DIR}`);
|
|
7734
|
-
console.log("Авторизуйтесь в открывшемся окне. Когда закончите, закройте окно браузера.");
|
|
7735
|
-
await runPersistentBrowserAutomation("open", {
|
|
7736
|
-
url,
|
|
7737
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7738
|
-
headed: true,
|
|
7739
|
-
waitMs: Number(options.wait || 0),
|
|
7740
|
-
timeout: Number(options.timeout || 120000),
|
|
7741
|
-
viewport: options.viewport || "1366x768",
|
|
7742
|
-
});
|
|
7743
|
-
const secrets = await loadSecrets();
|
|
7744
|
-
secrets.gosuslugiBrowser = {
|
|
7745
|
-
mode: "personal-browser",
|
|
7746
|
-
profileDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7747
|
-
connectedAt: new Date().toISOString(),
|
|
7748
|
-
lastUrl: url,
|
|
7749
|
-
};
|
|
7750
|
-
await saveSecrets(secrets);
|
|
7751
|
-
console.log("Локальный браузерный профиль Госуслуг сохранен.");
|
|
7752
|
-
}
|
|
7753
|
-
|
|
7754
|
-
async function gosuslugiBrowserOpen(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
|
|
7755
|
-
await requireGosuslugiConsent();
|
|
7756
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7757
|
-
await runPersistentBrowserAutomation("open", {
|
|
7758
|
-
url,
|
|
7759
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7760
|
-
headed: true,
|
|
7761
|
-
waitMs: Number(options.wait || 0),
|
|
7762
|
-
timeout: Number(options.timeout || 120000),
|
|
7763
|
-
viewport: options.viewport || "1366x768",
|
|
7764
|
-
});
|
|
7765
|
-
}
|
|
7766
|
-
|
|
7767
|
-
async function gosuslugiBrowserReadText(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
|
|
7768
|
-
await requireGosuslugiConsent();
|
|
7769
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7770
|
-
return runPersistentBrowserAutomation("text", {
|
|
7771
|
-
url,
|
|
7772
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7773
|
-
headed: Boolean(options.headed),
|
|
7774
|
-
waitMs: Number(options.wait || 3000),
|
|
7775
|
-
timeout: Number(options.timeout || 60000),
|
|
7776
|
-
viewport: options.viewport || "1366x768",
|
|
7777
|
-
});
|
|
7778
|
-
}
|
|
7779
|
-
|
|
7780
|
-
async function gosuslugiBrowserScreenshot(url = GOSUSLUGI_DEFAULT_URL, outputFile, options = {}) {
|
|
7781
|
-
await requireGosuslugiConsent();
|
|
7782
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7783
|
-
await runPersistentBrowserAutomation("screenshot", {
|
|
7784
|
-
url,
|
|
7785
|
-
output: outputFile,
|
|
7786
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7787
|
-
headed: Boolean(options.headed),
|
|
7788
|
-
waitMs: Number(options.wait || 3000),
|
|
7789
|
-
timeout: Number(options.timeout || 60000),
|
|
7790
|
-
viewport: options.viewport || "1366x768",
|
|
7791
|
-
});
|
|
7792
|
-
}
|
|
7793
|
-
|
|
7794
|
-
async function gosuslugiWhoami(options = {}) {
|
|
7795
|
-
const data = await gosuslugiBrowserApiJson({
|
|
7796
|
-
pageUrl: "https://lk.gosuslugi.ru/settings/account",
|
|
7797
|
-
endpoint: "https://www.gosuslugi.ru/api/lk/v1/users/data",
|
|
7798
|
-
waitMs: Number(options.wait || 3000),
|
|
7799
|
-
});
|
|
7800
|
-
const person = data.person?.person || data.person || data;
|
|
7801
|
-
const summary = {
|
|
7802
|
-
fio: [data.lastName || person.lastName, data.firstName || person.firstName, data.middleName || person.middleName].filter(Boolean).join(" ") || data.formattedName || "-",
|
|
7803
|
-
birthDate: person.birthDate || data.birthDate || "-",
|
|
7804
|
-
status: data.assuranceLevel === "AL20" || person.trusted ? "Подтвержденная учетная запись" : data.assuranceLevel || "-",
|
|
7805
|
-
phone: options.full ? (data.personMobilePhone || data.mobile || "-") : maskPhone(data.personMobilePhone || data.mobile || ""),
|
|
7806
|
-
email: options.full ? (data.personEMail || data.personEmail || data.email || "-") : maskEmail(data.personEMail || data.personEmail || data.email || ""),
|
|
7807
|
-
snils: options.full ? (person.snils || data.personSnils || data.snils || "-") : maskDocument(person.snils || data.personSnils || data.snils || ""),
|
|
7808
|
-
inn: options.full ? (person.inn || data.personINN || data.inn || "-") : maskDocument(person.inn || data.personINN || data.inn || ""),
|
|
7809
|
-
};
|
|
7810
|
-
return {
|
|
7811
|
-
summary,
|
|
7812
|
-
raw: options.full ? redactGosuslugiSensitive(data, { keepPersonal: true }) : undefined,
|
|
7813
|
-
};
|
|
7814
|
-
}
|
|
7815
|
-
|
|
7816
|
-
async function gosuslugiDebt(options = {}) {
|
|
7817
|
-
const data = await gosuslugiBrowserApiJson({
|
|
7818
|
-
pageUrl: "https://www.gosuslugi.ru/pay/forPayment",
|
|
7819
|
-
endpoint: "https://www.gosuslugi.ru/api/pay/v2/informer/fetch",
|
|
7820
|
-
waitMs: Number(options.wait || 5000),
|
|
7821
|
-
});
|
|
7822
|
-
const groups = Array.isArray(data.groups) ? data.groups : [];
|
|
7823
|
-
const debts = groups.flatMap((group) => (group.bills || []).map((bill) => ({
|
|
7824
|
-
group: group.name || group.code || "-",
|
|
7825
|
-
caption: bill.caption || "-",
|
|
7826
|
-
amount: Number(bill.amount || 0),
|
|
7827
|
-
billDate: bill.billDate || "-",
|
|
7828
|
-
supplier: bill.supplierFullName || "-",
|
|
7829
|
-
document: bill.document?.typeName ? `${bill.document.typeName} ${bill.document.number || ""}`.trim() : "-",
|
|
7830
|
-
})));
|
|
7831
|
-
return {
|
|
7832
|
-
total: Number(data.summary?.total || debts.length || 0),
|
|
7833
|
-
amount: Number(data.summary?.amount || debts.reduce((sum, item) => sum + item.amount, 0)),
|
|
7834
|
-
groups: groups.map((group) => ({ name: group.name, code: group.code, total: group.summary?.total || 0, amount: group.summary?.amount || 0 })),
|
|
7835
|
-
debts,
|
|
7836
|
-
};
|
|
7837
|
-
}
|
|
7838
|
-
|
|
7839
|
-
async function gosuslugiNotifications(options = {}) {
|
|
7840
|
-
const types = "ORDER,EQUEUE,PAYMENT,GEPS,BIOMETRICS,ACCOUNT,ACCOUNT_CHILD,PROFILE,APPEAL,CLAIM,ELECTION_INFO,COMPLEX_ORDER,FEEDBACK,ORGANIZATION,BUSINESSMAN,ESIGNATURE,KND_APPEAL,LINKED_ACCOUNT,SIGN,GOSQR,INFO,PERMISSION,LICENSING,LICENSING_APPEAL,CONSTRUCTOR";
|
|
7841
|
-
const pageSize = Number(options.limit || 15);
|
|
7842
|
-
const unread = options.unread ? "true" : "false";
|
|
7843
|
-
const counters = await gosuslugiBrowserApiJson({
|
|
7844
|
-
pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
|
|
7845
|
-
endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/counters?types=${types},PARTNERS&isArchive=false`,
|
|
7846
|
-
waitMs: Number(options.wait || 3000),
|
|
7847
|
-
});
|
|
7848
|
-
const feed = await gosuslugiBrowserApiJson({
|
|
7849
|
-
pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
|
|
7850
|
-
endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/?unread=${unread}&isArchive=false&isHide=false&types=${types}&pageSize=${pageSize}&status=&startDate=&lastFeedId=&lastFeedDate=&q=`,
|
|
7851
|
-
waitMs: Number(options.wait || 3000),
|
|
7852
|
-
});
|
|
7853
|
-
const items = (feed.items || []).map((item) => ({
|
|
7854
|
-
id: item.id,
|
|
7855
|
-
unread: Boolean(item.unread),
|
|
7856
|
-
date: item.date || "-",
|
|
7857
|
-
type: item.feedType || "-",
|
|
7858
|
-
title: item.title || "-",
|
|
7859
|
-
subtitle: item.subTitle || "-",
|
|
7860
|
-
status: item.status || "-",
|
|
7861
|
-
summary: summarizeNotificationData(item.data),
|
|
7862
|
-
}));
|
|
7863
|
-
return {
|
|
7864
|
-
total: counters.total || feed.items?.length || 0,
|
|
7865
|
-
unread: counters.unread || items.filter((item) => item.unread).length,
|
|
7866
|
-
counters: counters.counter || [],
|
|
7867
|
-
hasMore: Boolean(feed.hasMore),
|
|
7868
|
-
items,
|
|
7869
|
-
};
|
|
7870
|
-
}
|
|
7871
|
-
|
|
7872
|
-
async function gosuslugiMarkNotificationsRead(options = {}) {
|
|
7873
|
-
await requireGosuslugiConsent();
|
|
7874
|
-
if (!options.yes) {
|
|
7875
|
-
const ok = await confirm("Отметить уведомления Госуслуг прочитанными? Это изменит состояние личного кабинета. [y/N] ");
|
|
7876
|
-
if (!ok) {
|
|
7877
|
-
console.log("Операция отменена.");
|
|
7878
|
-
return;
|
|
7879
|
-
}
|
|
7880
|
-
}
|
|
7881
|
-
await gosuslugiBrowserClickText({
|
|
7882
|
-
pageUrl: "https://lk.gosuslugi.ru/notifications?type=ORDER,EQUEUE,PAYMENT,GEPS,BIOMETRICS,ACCOUNT,ACCOUNT_CHILD,PROFILE,APPEAL,CLAIM,ELECTION_INFO,COMPLEX_ORDER,FEEDBACK,ORGANIZATION,BUSINESSMAN,ESIGNATURE,KND_APPEAL,LINKED_ACCOUNT,SIGN,GOSQR,INFO,PERMISSION,LICENSING,LICENSING_APPEAL,CONSTRUCTOR",
|
|
7883
|
-
text: "Прочитать все",
|
|
7884
|
-
waitMs: Number(options.wait || 5000),
|
|
7885
|
-
});
|
|
7886
|
-
console.log("Команда отметки прочитанным выполнена. Проверьте статус: iola gosuslugi notifications --unread");
|
|
7887
|
-
}
|
|
7888
|
-
|
|
7889
|
-
async function gosuslugiCheck(options = {}) {
|
|
7890
|
-
try {
|
|
7891
|
-
const result = await gosuslugiWhoami({ wait: options.wait || 2000 });
|
|
7892
|
-
return {
|
|
7893
|
-
status: "ok",
|
|
7894
|
-
authorized: "yes",
|
|
7895
|
-
fio: result.summary.fio,
|
|
7896
|
-
checkedAt: new Date().toISOString(),
|
|
7897
|
-
nextAction: "-",
|
|
7898
|
-
};
|
|
7899
|
-
} catch (error) {
|
|
7900
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7901
|
-
const result = {
|
|
7902
|
-
status: "needs-login",
|
|
7903
|
-
authorized: "unknown",
|
|
7904
|
-
checkedAt: new Date().toISOString(),
|
|
7905
|
-
nextAction: "iola gosuslugi connect",
|
|
7906
|
-
error: message,
|
|
7907
|
-
};
|
|
7908
|
-
if (!options.silent) {
|
|
7909
|
-
console.error("Сессия Госуслуг недоступна или требует повторный вход.");
|
|
7910
|
-
console.error("Запустите: iola gosuslugi connect");
|
|
7911
|
-
}
|
|
7912
|
-
return result;
|
|
7913
|
-
}
|
|
7914
|
-
}
|
|
7915
|
-
|
|
7916
|
-
async function gosuslugiKeepalive(options = {}) {
|
|
7917
|
-
const intervalMs = parseDurationMs(options.interval || "30m");
|
|
7918
|
-
const once = Boolean(options.once);
|
|
7919
|
-
console.log(`Gosuslugi keepalive запущен. Интервал: ${Math.round(intervalMs / 60000)} мин.`);
|
|
7920
|
-
console.log("Остановить: Ctrl+C");
|
|
7921
|
-
while (true) {
|
|
7922
|
-
const result = await gosuslugiCheck({ silent: true });
|
|
7923
|
-
const line = result.status === "ok"
|
|
7924
|
-
? `[${result.checkedAt}] Госуслуги: сессия активна (${result.fio || "-"})`
|
|
7925
|
-
: `[${result.checkedAt}] Госуслуги: нужен повторный вход. Запустите: iola gosuslugi connect`;
|
|
7926
|
-
console.log(line);
|
|
7927
|
-
if (once) return;
|
|
7928
|
-
await sleep(intervalMs);
|
|
7929
|
-
}
|
|
7930
|
-
}
|
|
7931
|
-
|
|
7932
|
-
function gosuslugiKeepaliveTaskName() {
|
|
7933
|
-
return "iola-gosuslugi-keepalive";
|
|
7934
|
-
}
|
|
7935
|
-
|
|
7936
|
-
function gosuslugiKeepaliveLogFile() {
|
|
7937
|
-
return path.join(CONFIG_DIR, "gosuslugi-keepalive.log");
|
|
7938
|
-
}
|
|
7939
|
-
|
|
7940
|
-
function cliEntrypointFile() {
|
|
7941
|
-
return path.resolve(__dirname, "..", "bin", "iola.js");
|
|
7942
|
-
}
|
|
7943
|
-
|
|
7944
|
-
async function installGosuslugiKeepaliveTask(options = {}) {
|
|
7945
|
-
const intervalMinutes = Math.max(1, Math.round(parseDurationMs(options.interval || "30m") / 60000));
|
|
7946
|
-
if (process.platform === "win32") {
|
|
7947
|
-
await installWindowsGosuslugiKeepaliveTask(intervalMinutes);
|
|
7948
|
-
return;
|
|
7949
|
-
}
|
|
7950
|
-
const id = addCronJob(`каждые ${intervalMinutes} минут`, "gosuslugi check --silent");
|
|
7951
|
-
console.log(`Локальная cron-задача добавлена: ${id}`);
|
|
7952
|
-
console.log("Для автоматического выполнения настройте системный планировщик на запуск: iola cron tick");
|
|
7953
|
-
}
|
|
7954
|
-
|
|
7955
|
-
async function installWindowsGosuslugiKeepaliveTask(intervalMinutes) {
|
|
7956
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
7957
|
-
const taskName = gosuslugiKeepaliveTaskName();
|
|
7958
|
-
const logFile = gosuslugiKeepaliveLogFile();
|
|
7959
|
-
const script = path.join(CONFIG_DIR, "gosuslugi-keepalive-task.cmd");
|
|
7960
|
-
const command = `"${process.execPath}" --no-warnings "${cliEntrypointFile()}" gosuslugi check --silent >> "${logFile}" 2>&1`;
|
|
7961
|
-
await writeFile(script, `@echo off\r\n${command}\r\n`, "utf8");
|
|
7962
|
-
await runCommand("schtasks.exe", [
|
|
7963
|
-
"/Create",
|
|
7964
|
-
"/TN", taskName,
|
|
7965
|
-
"/SC", "MINUTE",
|
|
7966
|
-
"/MO", String(intervalMinutes),
|
|
7967
|
-
"/TR", script,
|
|
7968
|
-
"/F",
|
|
7969
|
-
]);
|
|
7970
|
-
console.log(`Windows Task Scheduler задача создана: ${taskName}`);
|
|
7971
|
-
console.log(`Интервал: ${intervalMinutes} мин.`);
|
|
7972
|
-
console.log(`Лог: ${logFile}`);
|
|
7973
|
-
console.log("Проверить: iola gosuslugi keepalive-status");
|
|
7974
|
-
}
|
|
7975
|
-
|
|
7976
|
-
async function uninstallGosuslugiKeepaliveTask() {
|
|
7977
|
-
if (process.platform === "win32") {
|
|
7978
|
-
await runCommand("schtasks.exe", ["/Delete", "/TN", gosuslugiKeepaliveTaskName(), "/F"]).catch(() => {});
|
|
7979
|
-
console.log(`Windows Task Scheduler задача удалена: ${gosuslugiKeepaliveTaskName()}`);
|
|
7980
|
-
return;
|
|
7981
|
-
}
|
|
7982
|
-
console.log("Для не-Windows удалите локальную cron-задачу вручную: iola cron list, затем iola cron delete ID.");
|
|
7983
|
-
}
|
|
7984
|
-
|
|
7985
|
-
async function printGosuslugiKeepaliveTaskStatus(options = {}) {
|
|
7986
|
-
if (process.platform === "win32") {
|
|
7987
|
-
try {
|
|
7988
|
-
const { stdout } = await runCommand("schtasks.exe", ["/Query", "/TN", gosuslugiKeepaliveTaskName(), "/FO", "LIST"]);
|
|
7989
|
-
console.log(stdout.trim());
|
|
7990
|
-
} catch {
|
|
7991
|
-
console.log(`Задача не найдена: ${gosuslugiKeepaliveTaskName()}`);
|
|
7992
|
-
}
|
|
7993
|
-
if (existsSync(gosuslugiKeepaliveLogFile())) {
|
|
7994
|
-
console.log("");
|
|
7995
|
-
console.log(`Лог: ${gosuslugiKeepaliveLogFile()}`);
|
|
7996
|
-
}
|
|
7997
|
-
return;
|
|
7998
|
-
}
|
|
7999
|
-
const rows = listCronJobs().filter((job) => String(job.command).includes("gosuslugi check"));
|
|
8000
|
-
if (options.json) printJson(rows);
|
|
8001
|
-
else printTable(rows, [["id", "ID"], ["enabled", "Вкл"], ["schedule_text", "Расписание"], ["command", "Команда"], ["last_run_at", "Последний запуск"]]);
|
|
8002
|
-
}
|
|
8003
|
-
|
|
8004
|
-
function parseDurationMs(value) {
|
|
8005
|
-
const text = String(value || "30m").trim().toLocaleLowerCase("ru-RU");
|
|
8006
|
-
const match = text.match(/^(\d+(?:[.,]\d+)?)(ms|s|m|h|мин|минут|час|часа|часов)?$/u);
|
|
8007
|
-
if (!match) throw new Error("Интервал задается как 30m, 1800s или 1h.");
|
|
8008
|
-
const amount = Number(match[1].replace(",", "."));
|
|
8009
|
-
const unit = match[2] || "m";
|
|
8010
|
-
if (unit === "ms") return Math.max(1000, amount);
|
|
8011
|
-
if (unit === "s") return Math.max(1000, amount * 1000);
|
|
8012
|
-
if (unit === "h" || unit.startsWith("час")) return Math.max(1000, amount * 60 * 60 * 1000);
|
|
8013
|
-
return Math.max(1000, amount * 60 * 1000);
|
|
8014
|
-
}
|
|
8015
|
-
|
|
8016
|
-
function printGosuslugiDebt(result) {
|
|
8017
|
-
printKeyValue({
|
|
8018
|
-
total: result.total,
|
|
8019
|
-
amount: `${formatRub(result.amount)} Р`,
|
|
8020
|
-
});
|
|
8021
|
-
if (!result.debts.length) {
|
|
8022
|
-
console.log("Задолженности не найдены.");
|
|
8023
|
-
return;
|
|
8024
|
-
}
|
|
8025
|
-
printTable(result.debts.map((item) => ({
|
|
8026
|
-
group: item.group,
|
|
8027
|
-
amount: `${formatRub(item.amount)} Р`,
|
|
8028
|
-
date: item.billDate,
|
|
8029
|
-
caption: item.caption,
|
|
8030
|
-
})), [
|
|
8031
|
-
["group", "Группа"],
|
|
8032
|
-
["amount", "Сумма"],
|
|
8033
|
-
["date", "Дата"],
|
|
8034
|
-
["caption", "Описание"],
|
|
8035
|
-
]);
|
|
8036
|
-
}
|
|
8037
|
-
|
|
8038
|
-
function printGosuslugiNotifications(result) {
|
|
8039
|
-
printKeyValue({ total: result.total, unread: result.unread, hasMore: result.hasMore ? "yes" : "no" });
|
|
8040
|
-
printTable(result.items.map((item) => ({
|
|
8041
|
-
unread: item.unread ? "new" : "read",
|
|
8042
|
-
date: item.date,
|
|
8043
|
-
type: item.type,
|
|
8044
|
-
title: item.title,
|
|
8045
|
-
subtitle: item.subtitle,
|
|
8046
|
-
summary: item.summary,
|
|
8047
|
-
})), [
|
|
8048
|
-
["unread", "Статус"],
|
|
8049
|
-
["date", "Дата"],
|
|
8050
|
-
["type", "Тип"],
|
|
8051
|
-
["title", "Заголовок"],
|
|
8052
|
-
["subtitle", "Подзаголовок"],
|
|
8053
|
-
["summary", "Детали"],
|
|
8054
|
-
]);
|
|
8055
|
-
}
|
|
8056
|
-
|
|
8057
|
-
function summarizeNotificationData(data) {
|
|
8058
|
-
if (!data || typeof data !== "object") return "";
|
|
8059
|
-
const snippets = Array.isArray(data.snippets) ? data.snippets : [];
|
|
8060
|
-
if (snippets.length) {
|
|
8061
|
-
const first = snippets[0];
|
|
8062
|
-
return [first.orgName, first.address, first.date].filter(Boolean).join(" | ");
|
|
8063
|
-
}
|
|
8064
|
-
return [data.messageType, data.messageUuid, data.orderId, data.passCodeEpguCode].filter(Boolean).join(" | ");
|
|
8065
|
-
}
|
|
8066
|
-
|
|
8067
|
-
function formatRub(value) {
|
|
8068
|
-
return Number(value || 0).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
8069
|
-
}
|
|
8070
|
-
|
|
8071
|
-
function isGosuslugiPersonalIntent(question) {
|
|
8072
|
-
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
8073
|
-
return /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized);
|
|
8074
|
-
}
|
|
8075
|
-
|
|
8076
|
-
async function answerGosuslugiQuestion(question, options = {}) {
|
|
8077
|
-
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
8078
|
-
if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
|
|
8079
|
-
const result = await gosuslugiNotifications({ unread: /непрочитан|нов/iu.test(normalized), limit: options.limit || 10 });
|
|
8080
|
-
const lines = [`На Госуслугах: всего уведомлений ${result.total}, непрочитанных ${result.unread}.`];
|
|
8081
|
-
const items = result.items.slice(0, Number(options.limit || 5));
|
|
8082
|
-
if (items.length) {
|
|
8083
|
-
lines.push("");
|
|
8084
|
-
for (const item of items) {
|
|
8085
|
-
lines.push(`- ${item.unread ? "новое" : "прочитано"}: ${item.title} — ${item.subtitle} (${item.date})`);
|
|
8086
|
-
}
|
|
8087
|
-
}
|
|
8088
|
-
return lines.join("\n");
|
|
8089
|
-
}
|
|
8090
|
-
if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
|
|
8091
|
-
const result = await gosuslugiDebt(options);
|
|
8092
|
-
if (!result.debts.length) return "На Госуслугах задолженности к оплате не найдены.";
|
|
8093
|
-
const lines = [`На Госуслугах найдено задолженностей: ${result.total}. Общая сумма: ${formatRub(result.amount)} Р.`];
|
|
8094
|
-
for (const item of result.debts) {
|
|
8095
|
-
lines.push(`- ${item.group}: ${formatRub(item.amount)} Р — ${item.caption}`);
|
|
8096
|
-
}
|
|
8097
|
-
return lines.join("\n");
|
|
8098
|
-
}
|
|
8099
|
-
const result = await gosuslugiWhoami(options);
|
|
8100
|
-
return [
|
|
8101
|
-
`ФИО: ${result.summary.fio}`,
|
|
8102
|
-
`Дата рождения: ${result.summary.birthDate}`,
|
|
8103
|
-
`Статус: ${result.summary.status}`,
|
|
8104
|
-
].join("\n");
|
|
8105
|
-
}
|
|
8106
|
-
|
|
8107
|
-
function maskPhone(value) {
|
|
8108
|
-
const text = String(value || "");
|
|
8109
|
-
return text.replace(/(\+?\d)([\d\s()-]{4,})(\d{2})$/u, "$1***$3") || "-";
|
|
8110
|
-
}
|
|
8111
|
-
|
|
8112
|
-
function maskEmail(value) {
|
|
8113
|
-
const text = String(value || "");
|
|
8114
|
-
const [name, domain] = text.split("@");
|
|
8115
|
-
if (!name || !domain) return text || "-";
|
|
8116
|
-
return `${name.slice(0, 2)}***@${domain}`;
|
|
8117
|
-
}
|
|
8118
|
-
|
|
8119
|
-
function maskDocument(value) {
|
|
8120
|
-
const digits = String(value || "").replace(/\D+/g, "");
|
|
8121
|
-
if (!digits) return "-";
|
|
8122
|
-
return `***${digits.slice(-4)}`;
|
|
8123
|
-
}
|
|
8124
|
-
|
|
8125
|
-
function redactGosuslugiSensitive(value, options = {}) {
|
|
8126
|
-
if (Array.isArray(value)) return value.map((item) => redactGosuslugiSensitive(item, options));
|
|
8127
|
-
if (!value || typeof value !== "object") return value;
|
|
8128
|
-
const result = {};
|
|
8129
|
-
for (const [key, item] of Object.entries(value)) {
|
|
8130
|
-
if (/token|cookie|session|password|secret|jwt|auth/i.test(key)) result[key] = "[redacted]";
|
|
8131
|
-
else if (!options.keepPersonal && /(snils|inn|passport|number|series|address|mobile|email|phone)/i.test(key)) result[key] = "[redacted]";
|
|
8132
|
-
else result[key] = redactGosuslugiSensitive(item, options);
|
|
8133
|
-
}
|
|
8134
|
-
return result;
|
|
8135
|
-
}
|
|
8136
|
-
|
|
8137
|
-
async function runPersistentBrowserAutomation(action, params) {
|
|
8138
|
-
await ensureBrowserRuntime();
|
|
8139
|
-
await mkdir(params.userDataDir, { recursive: true });
|
|
8140
|
-
const releaseLock = params.userDataDir === GOSUSLUGI_BROWSER_PROFILE_DIR ? await acquireDirectoryLock(GOSUSLUGI_BROWSER_LOCK_DIR, 180000) : async () => {};
|
|
8141
|
-
const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-profile-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
|
|
8142
|
-
await writeFile(scriptFile, persistentBrowserAutomationScript(action, params), "utf8");
|
|
8143
|
-
try {
|
|
8144
|
-
const options = action === "open" ? { cwd: BROWSER_RUNTIME_DIR, inherit: true } : { cwd: BROWSER_RUNTIME_DIR };
|
|
8145
|
-
const result = await runCommand(process.execPath, [scriptFile], options);
|
|
8146
|
-
return result.stdout?.trim() || "";
|
|
8147
|
-
} finally {
|
|
8148
|
-
await rm(scriptFile, { force: true }).catch(() => {});
|
|
8149
|
-
await releaseLock();
|
|
8150
|
-
}
|
|
8151
|
-
}
|
|
8152
|
-
|
|
8153
|
-
async function acquireDirectoryLock(lockDir, timeoutMs = 60000) {
|
|
8154
|
-
const started = Date.now();
|
|
8155
|
-
while (true) {
|
|
8156
|
-
try {
|
|
8157
|
-
await mkdir(lockDir, { recursive: false });
|
|
8158
|
-
await writeFile(path.join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2), "utf8").catch(() => {});
|
|
8159
|
-
return async () => {
|
|
8160
|
-
await rm(lockDir, { recursive: true, force: true }).catch(() => {});
|
|
8161
|
-
};
|
|
8162
|
-
} catch {
|
|
8163
|
-
if (Date.now() - started > timeoutMs) {
|
|
8164
|
-
throw new Error("Браузерный профиль Госуслуг занят другим процессом. Закройте окно Госуслуг или повторите команду позже.");
|
|
8165
|
-
}
|
|
8166
|
-
await sleep(1000);
|
|
8167
|
-
}
|
|
8168
|
-
}
|
|
8169
|
-
}
|
|
8170
|
-
|
|
8171
|
-
async function gosuslugiBrowserApiJson(params) {
|
|
8172
|
-
await requireGosuslugiConsent();
|
|
8173
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
8174
|
-
const raw = await runPersistentBrowserAutomation("api-json", {
|
|
8175
|
-
pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
|
|
8176
|
-
endpoint: params.endpoint,
|
|
8177
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
8178
|
-
headed: params.headed !== false,
|
|
8179
|
-
waitMs: Number(params.waitMs || 0),
|
|
8180
|
-
timeout: Number(params.timeout || 60000),
|
|
8181
|
-
viewport: params.viewport || "1366x768",
|
|
8182
|
-
});
|
|
8183
|
-
return JSON.parse(raw);
|
|
8184
|
-
}
|
|
8185
|
-
|
|
8186
|
-
async function gosuslugiBrowserClickText(params) {
|
|
8187
|
-
await requireGosuslugiConsent();
|
|
8188
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
8189
|
-
return runPersistentBrowserAutomation("click-text", {
|
|
8190
|
-
pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
|
|
8191
|
-
text: params.text,
|
|
8192
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
8193
|
-
headed: true,
|
|
8194
|
-
waitMs: Number(params.waitMs || 3000),
|
|
8195
|
-
timeout: Number(params.timeout || 60000),
|
|
8196
|
-
viewport: params.viewport || "1366x768",
|
|
8197
|
-
});
|
|
8198
|
-
}
|
|
8199
|
-
|
|
8200
|
-
function persistentBrowserAutomationScript(action, params) {
|
|
8201
|
-
return `
|
|
8202
|
-
import { chromium } from "playwright";
|
|
8203
|
-
const action = ${JSON.stringify(action)};
|
|
8204
|
-
const params = ${JSON.stringify(params)};
|
|
8205
|
-
const [width, height] = String(params.viewport || "1366x768").split("x").map(Number);
|
|
8206
|
-
const context = await chromium.launchPersistentContext(params.userDataDir, {
|
|
8207
|
-
headless: !params.headed,
|
|
8208
|
-
viewport: { width: width || 1366, height: height || 768 },
|
|
8209
|
-
});
|
|
8210
|
-
context.setDefaultTimeout(params.timeout || 60000);
|
|
8211
|
-
const page = context.pages()[0] || await context.newPage();
|
|
8212
|
-
try {
|
|
8213
|
-
await page.goto(params.url || params.pageUrl, { waitUntil: "domcontentloaded", timeout: params.timeout || 60000 });
|
|
8214
|
-
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
8215
|
-
if (action === "open") {
|
|
8216
|
-
if (params.headed) {
|
|
8217
|
-
page.on("close", async () => {
|
|
8218
|
-
await context.close().catch(() => {});
|
|
8219
|
-
});
|
|
8220
|
-
while (!page.isClosed()) {
|
|
8221
|
-
await page.waitForTimeout(1000).catch(() => {});
|
|
8222
|
-
}
|
|
8223
|
-
}
|
|
8224
|
-
} else if (action === "text") {
|
|
8225
|
-
console.log((await page.locator("body").innerText()).trim());
|
|
8226
|
-
} else if (action === "screenshot") {
|
|
8227
|
-
await page.screenshot({ path: params.output, fullPage: true });
|
|
8228
|
-
} else if (action === "api-json") {
|
|
8229
|
-
const data = await page.evaluate(async (endpoint) => {
|
|
8230
|
-
const response = await fetch(endpoint, {
|
|
8231
|
-
credentials: "include",
|
|
8232
|
-
headers: { accept: "application/json" },
|
|
8233
|
-
});
|
|
8234
|
-
const text = await response.text();
|
|
8235
|
-
if (!response.ok) throw new Error(response.status + " " + response.statusText + ": " + text.slice(0, 500));
|
|
8236
|
-
return JSON.parse(text);
|
|
8237
|
-
}, params.endpoint);
|
|
8238
|
-
console.log(JSON.stringify(data));
|
|
8239
|
-
} else if (action === "click-text") {
|
|
8240
|
-
await page.getByText(params.text, { exact: true }).first().click();
|
|
8241
|
-
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
8242
|
-
console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
|
|
8243
|
-
}
|
|
8244
|
-
} finally {
|
|
8245
|
-
await context.close().catch(() => {});
|
|
8246
|
-
}
|
|
8247
|
-
`;
|
|
8248
|
-
}
|
|
8249
|
-
|
|
8250
7493
|
function browserAutomationScript(action, params) {
|
|
8251
7494
|
return `
|
|
8252
7495
|
import { chromium } from "playwright";
|
|
@@ -8477,9 +7720,6 @@ function mcpTools() {
|
|
|
8477
7720
|
{ name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
|
|
8478
7721
|
{ name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
|
|
8479
7722
|
{ name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
|
|
8480
|
-
{ name: "gosuslugi.whoami", description: "Прочитать ФИО и дату рождения из личного профиля Госуслуг через локальный браузерный профиль.", inputSchema: schema({ full: { type: "boolean" } }) },
|
|
8481
|
-
{ name: "gosuslugi.debt", description: "Прочитать задолженности и платежи к оплате на Госуслугах.", inputSchema: schema() },
|
|
8482
|
-
{ name: "gosuslugi.notifications", description: "Прочитать уведомления Госуслуг.", inputSchema: schema({ unread: { type: "boolean" }, limit: { type: "number" } }) },
|
|
8483
7723
|
];
|
|
8484
7724
|
}
|
|
8485
7725
|
|
|
@@ -8517,9 +7757,6 @@ async function callMcpTool(name, args = {}) {
|
|
|
8517
7757
|
await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
|
|
8518
7758
|
return { output };
|
|
8519
7759
|
}
|
|
8520
|
-
if (name === "gosuslugi.whoami") return gosuslugiWhoami(args);
|
|
8521
|
-
if (name === "gosuslugi.debt") return gosuslugiDebt(args);
|
|
8522
|
-
if (name === "gosuslugi.notifications") return gosuslugiNotifications(args);
|
|
8523
7760
|
return executeRpc(name, { ...args, _: [] });
|
|
8524
7761
|
}
|
|
8525
7762
|
|
|
@@ -9598,10 +8835,6 @@ function mergeConfig(base, override) {
|
|
|
9598
8835
|
...base.api,
|
|
9599
8836
|
...(override.api || {}),
|
|
9600
8837
|
},
|
|
9601
|
-
gosuslugi: {
|
|
9602
|
-
...base.gosuslugi,
|
|
9603
|
-
...(override.gosuslugi || {}),
|
|
9604
|
-
},
|
|
9605
8838
|
ai: {
|
|
9606
8839
|
...base.ai,
|
|
9607
8840
|
...(override.ai || {}),
|
|
@@ -9684,11 +8917,6 @@ function validateConfig(config) {
|
|
|
9684
8917
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
9685
8918
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
9686
8919
|
}
|
|
9687
|
-
if (config.gosuslugi?.enabled && !isGosuslugiConfigured(config)) {
|
|
9688
|
-
if ((config.gosuslugi?.mode || "personal-browser") !== "personal-browser") {
|
|
9689
|
-
errors.push("gosuslugi включен в OAuth/OIDC-режиме, но authUrl/tokenUrl/clientId не заполнены");
|
|
9690
|
-
}
|
|
9691
|
-
}
|
|
9692
8920
|
return errors;
|
|
9693
8921
|
}
|
|
9694
8922
|
|
|
@@ -9698,7 +8926,6 @@ function configSchema() {
|
|
|
9698
8926
|
required: ["api", "ai"],
|
|
9699
8927
|
properties: {
|
|
9700
8928
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
9701
|
-
gosuslugi: { modes: ["personal-browser", "personal-local"], browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR, oauthRequiredWhenEnabled: ["authUrl", "tokenUrl", "clientId"], optional: ["userinfoUrl", "clientSecret", "scope", "redirectHost", "redirectPort", "redirectPath"] },
|
|
9702
8929
|
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
9703
8930
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
9704
8931
|
toolsets: { available: Object.keys(TOOLSETS) },
|