@iola_adm/iola-cli 0.1.64 → 0.1.65
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 +1 -1
- package/src/cli.js +6 -1002
- 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" },
|
|
@@ -361,7 +322,6 @@ const COMMANDS = new Map([
|
|
|
361
322
|
["fork", forkSession],
|
|
362
323
|
["features", handleFeatures],
|
|
363
324
|
["settings", handleSettings],
|
|
364
|
-
["gosuslugi", handleGosuslugi],
|
|
365
325
|
["wiki", handleWiki],
|
|
366
326
|
["context", handleContext],
|
|
367
327
|
["skills", handleSkills],
|
|
@@ -489,7 +449,6 @@ async function showHelp() {
|
|
|
489
449
|
iola agent интерактивный режим
|
|
490
450
|
iola ai setup настройка AI-профиля
|
|
491
451
|
iola browser status браузерный runtime
|
|
492
|
-
iola gosuslugi status личное подключение Госуслуг
|
|
493
452
|
iola mcp status MCP-подключение
|
|
494
453
|
iola doctor диагностика
|
|
495
454
|
iola wiki документация
|
|
@@ -524,7 +483,6 @@ Usage:
|
|
|
524
483
|
iola fork SESSION_ID [TEXT]
|
|
525
484
|
iola features list|enable|disable
|
|
526
485
|
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
486
|
iola wiki [open|links]
|
|
529
487
|
iola context list|show|init
|
|
530
488
|
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
@@ -1059,10 +1017,6 @@ async function handleAgentLine(line, state) {
|
|
|
1059
1017
|
return false;
|
|
1060
1018
|
}
|
|
1061
1019
|
|
|
1062
|
-
if (command === "gosuslugi") {
|
|
1063
|
-
await handleGosuslugi(args);
|
|
1064
|
-
return false;
|
|
1065
|
-
}
|
|
1066
1020
|
|
|
1067
1021
|
if (command === "workspace") {
|
|
1068
1022
|
await handleWorkspace(args);
|
|
@@ -1213,7 +1167,6 @@ async function handleAgentLine(line, state) {
|
|
|
1213
1167
|
resume: ["resume", args],
|
|
1214
1168
|
fork: ["fork", args],
|
|
1215
1169
|
features: ["features", args],
|
|
1216
|
-
gosuslugi: ["gosuslugi", args],
|
|
1217
1170
|
wiki: ["wiki", args],
|
|
1218
1171
|
context: ["context", args],
|
|
1219
1172
|
skills: ["skills", args],
|
|
@@ -2245,185 +2198,6 @@ async function handleSettings(args) {
|
|
|
2245
2198
|
throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
|
|
2246
2199
|
}
|
|
2247
2200
|
|
|
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
2201
|
async function handleWiki(args) {
|
|
2428
2202
|
const [action = "links"] = args;
|
|
2429
2203
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -2439,7 +2213,6 @@ async function handleWiki(args) {
|
|
|
2439
2213
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
2440
2214
|
["Платформа агента", `${base}/Платформа-агента`],
|
|
2441
2215
|
["Браузерный агент", `${base}/Браузерный-агент`],
|
|
2442
|
-
["Подключение Госуслуг", `${base}/Подключение-Госуслуг`],
|
|
2443
2216
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
2444
2217
|
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
2445
2218
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
@@ -3376,189 +3149,6 @@ async function openUrl(url) {
|
|
|
3376
3149
|
await runCommand("xdg-open", [url], { inherit: false });
|
|
3377
3150
|
}
|
|
3378
3151
|
|
|
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
3152
|
function maskSecret(value) {
|
|
3563
3153
|
const text = String(value || "");
|
|
3564
3154
|
if (text.length <= 8) return text ? "***" : "-";
|
|
@@ -6075,12 +5665,6 @@ async function aiAsk(args, context = {}) {
|
|
|
6075
5665
|
throw new Error('Текст вопроса обязателен. Пример: iola ai ask "Какие школы есть на улице Петрова?"');
|
|
6076
5666
|
}
|
|
6077
5667
|
|
|
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
5668
|
const config = await loadConfig();
|
|
6085
5669
|
const providerConfig = await resolveUsableAiProfile(config, options);
|
|
6086
5670
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
@@ -6249,7 +5833,7 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
6249
5833
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
6250
5834
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
6251
5835
|
"Схема: {\"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}
|
|
5836
|
+
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
6253
5837
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
6254
5838
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
6255
5839
|
`Вопрос: ${question}`,
|
|
@@ -6279,12 +5863,6 @@ function inferToolPlan(question, options = {}) {
|
|
|
6279
5863
|
const steps = [];
|
|
6280
5864
|
if (normalized.includes("без телефона")) {
|
|
6281
5865
|
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
5866
|
} else {
|
|
6289
5867
|
const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
|
|
6290
5868
|
steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
|
|
@@ -6374,18 +5952,6 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6374
5952
|
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
5953
|
current = [{ url: step.args?.url, text }];
|
|
6376
5954
|
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
5955
|
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
6390
5956
|
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
6391
5957
|
current = Array.isArray(result) ? result : [result];
|
|
@@ -7122,17 +6688,6 @@ async function onboard(args = []) {
|
|
|
7122
6688
|
if (status.installed === "yes") console.log("Browser runtime уже установлен.");
|
|
7123
6689
|
else await installBrowserRuntime();
|
|
7124
6690
|
}
|
|
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
6691
|
if (components.includes("index")) {
|
|
7137
6692
|
await setFilesMode("read-only", await loadConfig());
|
|
7138
6693
|
console.log("Индекс документов можно запустить командой: iola index folder ./docs");
|
|
@@ -7166,7 +6721,6 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7166
6721
|
8: "archive",
|
|
7167
6722
|
9: "index",
|
|
7168
6723
|
10: "browser",
|
|
7169
|
-
11: "gosuslugi",
|
|
7170
6724
|
};
|
|
7171
6725
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
7172
6726
|
} finally {
|
|
@@ -7175,18 +6729,16 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7175
6729
|
}
|
|
7176
6730
|
|
|
7177
6731
|
async function getOnboardComponentStatus() {
|
|
7178
|
-
const [config, readiness, browser, archive, codexVersion, ollamaVersion
|
|
6732
|
+
const [config, readiness, browser, archive, codexVersion, ollamaVersion] = await Promise.all([
|
|
7179
6733
|
loadConfig(),
|
|
7180
6734
|
getAiReadiness(),
|
|
7181
6735
|
getBrowserStatus(),
|
|
7182
6736
|
findCommand(["7z", "7zz", "7za"], ["--help"]).catch(() => null),
|
|
7183
6737
|
getCommandVersion("codex", ["--version"]),
|
|
7184
6738
|
getOllamaVersion(),
|
|
7185
|
-
loadSecrets(),
|
|
7186
6739
|
]);
|
|
7187
6740
|
const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
|
|
7188
6741
|
const policyReady = (config.toolsets?.enabled || []).includes("analyst");
|
|
7189
|
-
const gosuslugiReady = Boolean(config.gosuslugi?.enabled && existsSync(GOSUSLUGI_BROWSER_PROFILE_DIR) && secrets.gosuslugiBrowser?.connectedAt);
|
|
7190
6742
|
return {
|
|
7191
6743
|
workspace: workspaceReady,
|
|
7192
6744
|
policy: policyReady,
|
|
@@ -7198,7 +6750,6 @@ async function getOnboardComponentStatus() {
|
|
|
7198
6750
|
archive: Boolean(archive),
|
|
7199
6751
|
index: false,
|
|
7200
6752
|
browser: browser.installed === "yes",
|
|
7201
|
-
gosuslugi: gosuslugiReady,
|
|
7202
6753
|
};
|
|
7203
6754
|
}
|
|
7204
6755
|
|
|
@@ -7214,7 +6765,6 @@ function onboardComponentRows(status) {
|
|
|
7214
6765
|
["8", "archive", "7-Zip / архивы", "архиватор найден"],
|
|
7215
6766
|
["9", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
|
|
7216
6767
|
["10", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
7217
|
-
["11", "gosuslugi", "Личное подключение Госуслуг", "профиль и keepalive"],
|
|
7218
6768
|
];
|
|
7219
6769
|
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
7220
6770
|
}
|
|
@@ -7228,7 +6778,7 @@ function defaultOnboardSelection(status) {
|
|
|
7228
6778
|
}
|
|
7229
6779
|
|
|
7230
6780
|
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"
|
|
6781
|
+
const map = { 1: "workspace", 2: "policy", 3: "ollama", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser" };
|
|
7232
6782
|
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
7233
6783
|
}
|
|
7234
6784
|
|
|
@@ -7478,8 +7028,7 @@ async function buildSkillsText(config, question = "", options = {}) {
|
|
|
7478
7028
|
const chunks = [];
|
|
7479
7029
|
const selected = selectSkillsForPrompt(config, question, options);
|
|
7480
7030
|
for (const skill of listSkills(config)) {
|
|
7481
|
-
|
|
7482
|
-
if (!active || !selected.has(skill.name)) continue;
|
|
7031
|
+
if (!skill.enabled || !selected.has(skill.name)) continue;
|
|
7483
7032
|
const text = await readFile(skill.file, "utf8");
|
|
7484
7033
|
chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
|
|
7485
7034
|
}
|
|
@@ -7495,7 +7044,6 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
7495
7044
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
7496
7045
|
if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
7497
7046
|
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
7047
|
return selected;
|
|
7500
7048
|
}
|
|
7501
7049
|
|
|
@@ -7719,534 +7267,6 @@ async function runBrowserAutomation(action, params) {
|
|
|
7719
7267
|
}
|
|
7720
7268
|
}
|
|
7721
7269
|
|
|
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
7270
|
function browserAutomationScript(action, params) {
|
|
8251
7271
|
return `
|
|
8252
7272
|
import { chromium } from "playwright";
|
|
@@ -8477,9 +7497,6 @@ function mcpTools() {
|
|
|
8477
7497
|
{ name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
|
|
8478
7498
|
{ name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
|
|
8479
7499
|
{ 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
7500
|
];
|
|
8484
7501
|
}
|
|
8485
7502
|
|
|
@@ -8517,9 +7534,6 @@ async function callMcpTool(name, args = {}) {
|
|
|
8517
7534
|
await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
|
|
8518
7535
|
return { output };
|
|
8519
7536
|
}
|
|
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
7537
|
return executeRpc(name, { ...args, _: [] });
|
|
8524
7538
|
}
|
|
8525
7539
|
|
|
@@ -9598,10 +8612,6 @@ function mergeConfig(base, override) {
|
|
|
9598
8612
|
...base.api,
|
|
9599
8613
|
...(override.api || {}),
|
|
9600
8614
|
},
|
|
9601
|
-
gosuslugi: {
|
|
9602
|
-
...base.gosuslugi,
|
|
9603
|
-
...(override.gosuslugi || {}),
|
|
9604
|
-
},
|
|
9605
8615
|
ai: {
|
|
9606
8616
|
...base.ai,
|
|
9607
8617
|
...(override.ai || {}),
|
|
@@ -9684,11 +8694,6 @@ function validateConfig(config) {
|
|
|
9684
8694
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
9685
8695
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
9686
8696
|
}
|
|
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
8697
|
return errors;
|
|
9693
8698
|
}
|
|
9694
8699
|
|
|
@@ -9698,7 +8703,6 @@ function configSchema() {
|
|
|
9698
8703
|
required: ["api", "ai"],
|
|
9699
8704
|
properties: {
|
|
9700
8705
|
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
8706
|
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
9703
8707
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
9704
8708
|
toolsets: { available: Object.keys(TOOLSETS) },
|