@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/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", "gosuslugi_whoami", "gosuslugi_debt", "gosuslugi_notifications"];
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", "gosuslugi"],
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}, gosuslugi_whoami {}, gosuslugi_debt {}, gosuslugi_notifications {unread,limit}.",
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, secrets] = await Promise.all([
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", 11: "gosuslugi" };
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
- const active = skill.enabled || (skill.name === "gosuslugi" && config.gosuslugi?.enabled);
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) },