@iola_adm/iola-cli 0.1.58 → 0.1.60

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 CHANGED
@@ -40,8 +40,7 @@ npx -y @iola_adm/iola-cli
40
40
  Повторный запуск мастера:
41
41
 
42
42
  ```bash
43
- iola wizard
44
- iola setup wizard
43
+ iola master
45
44
  ```
46
45
 
47
46
  Мастер обновляет только выбранные разделы и не сбрасывает остальные настройки.
@@ -86,6 +85,10 @@ iola trajectory last
86
85
  iola review config
87
86
  iola browser status
88
87
  iola gosuslugi status
88
+ iola gosuslugi connect
89
+ iola gosuslugi whoami
90
+ iola gosuslugi debt
91
+ iola gosuslugi notifications --unread
89
92
  ```
90
93
 
91
94
  Локальная модель через Ollama:
@@ -133,14 +136,15 @@ iola version --check
133
136
  - MCP-мост для локальной модели: встроенный `iola-local` доступен как `mcp:iola-local:TOOL`;
134
137
  - дополнительные stdio MCP-серверы можно добавить в `~/.iola/config.json` в раздел `mcp.servers`;
135
138
  - браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
136
- - личное локальное подключение Госуслуг с явным согласием пользователя и хранением доступа только на его ПК;
139
+ - личное локальное подключение Госуслуг через отдельный браузерный профиль на ПК пользователя;
140
+ - read-only tools Госуслуг для агента: ФИО, дата рождения, задолженности и уведомления;
137
141
  - управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
138
142
  - планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
139
143
  - экспорт отчетов в Excel/Word-совместимые файлы;
140
144
  - staged changes, импорт локальных CSV/JSON, индекс локальных документов, report packs, plugins и локальный MCP endpoint;
141
145
  - чтение и индексирование `.docx`, `.xlsx`, `.pptx`, `.pdf`, `.md`, `.txt`, `.csv`, `.json`, `.html`;
142
146
  - работа с архивами через 7-Zip: `.zip`, `.7z`, `.rar`, `.tar`, `.gz`, `.tgz`, `.bz2`, `.xz` и другие;
143
- - расширенный `iola onboard` с установкой 7-Zip, Ollama, Codex CLI и настройкой выбранных компонентов;
147
+ - расширенный `iola onboard` с установкой 7-Zip, браузерного runtime, Ollama, Codex CLI и настройкой выбранных компонентов;
144
148
  - cron-задачи, локальный daemon, web dashboard и RPC для автоматизаций;
145
149
  - контекстные файлы `IOLA.md` и `.iola/context.md`;
146
150
  - интеграция с публичным MCP-сервером Йошкар-Олы.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -16,7 +16,7 @@
16
16
  "iola": "bin/iola.js"
17
17
  },
18
18
  "scripts": {
19
- "postinstall": "node --no-warnings bin/iola.js db init --silent || true",
19
+ "postinstall": "node --no-warnings bin/iola.js db init --silent && node --no-warnings bin/iola.js browser install",
20
20
  "start": "node --no-warnings bin/iola.js",
21
21
  "test": "node --no-warnings --check bin/iola.js && node --no-warnings --check src/cli.js"
22
22
  },
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: gosuslugi
3
+ description: Работа с личным аккаунтом Госуслуг пользователя через локальный браузерный профиль.
4
+ ---
5
+
6
+ Используй этот skill только когда пользователь явно спрашивает про свой личный аккаунт Госуслуг: ФИО, дату рождения, задолженности, штрафы, налоги, платежи, уведомления или госпочту.
7
+
8
+ Доступные read-only tools:
9
+
10
+ - `gosuslugi_whoami` - получить ФИО, дату рождения и краткие данные профиля.
11
+ - `gosuslugi_debt` - получить задолженности и суммы к оплате.
12
+ - `gosuslugi_notifications` - получить уведомления, включая новые непрочитанные.
13
+
14
+ Не выполняй юридически значимые действия автоматически. Оплата, отправка заявлений, изменение персональных данных, удаление учетной записи, согласия и отметка уведомлений прочитанными требуют отдельного явного подтверждения пользователя.
15
+
16
+ Если сохраненная сессия Госуслуг протухла или требуется повторная двухфакторная аутентификация, попроси пользователя выполнить `iola gosuslugi connect`.
package/src/cli.js CHANGED
@@ -26,8 +26,11 @@ const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
26
26
  const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
27
27
  const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
28
28
  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/";
29
32
  const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
30
- const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
33
+ const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open", "gosuslugi_whoami", "gosuslugi_debt", "gosuslugi_notifications"];
31
34
  const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
32
35
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
33
36
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
@@ -130,7 +133,7 @@ const DEFAULT_AI_CONFIG = {
130
133
  },
131
134
  gosuslugi: {
132
135
  enabled: false,
133
- mode: "personal-local",
136
+ mode: "personal-browser",
134
137
  authUrl: "",
135
138
  tokenUrl: "",
136
139
  userinfoUrl: "",
@@ -178,6 +181,9 @@ const DEFAULT_AI_CONFIG = {
178
181
  export_report: true,
179
182
  file_read: false,
180
183
  browser_open: true,
184
+ gosuslugi_whoami: true,
185
+ gosuslugi_debt: true,
186
+ gosuslugi_notifications: true,
181
187
  files_tree: false,
182
188
  files_read: false,
183
189
  files_search: false,
@@ -208,7 +214,7 @@ const DEFAULT_AI_CONFIG = {
208
214
  suggestions: true,
209
215
  },
210
216
  skills: {
211
- enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
217
+ enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent", "gosuslugi"],
212
218
  },
213
219
  daemon: {
214
220
  host: "127.0.0.1",
@@ -281,6 +287,9 @@ const SLASH_COMMANDS = [
281
287
  { command: "/resume SESSION_ID", description: "продолжить сессию" },
282
288
  { command: "/features list", description: "feature flags" },
283
289
  { command: "/gosuslugi status", description: "личное подключение Госуслуг" },
290
+ { command: "/gosuslugi connect", description: "открыть личный вход Госуслуг" },
291
+ { command: "/gosuslugi debt", description: "задолженности Госуслуг" },
292
+ { command: "/gosuslugi notifications", description: "уведомления Госуслуг" },
284
293
  { command: "/wiki", description: "ссылки на документацию" },
285
294
  { command: "/context list", description: "локальный контекст проекта" },
286
295
  { command: "/skills list", description: "skills" },
@@ -514,7 +523,7 @@ Usage:
514
523
  iola fork SESSION_ID [TEXT]
515
524
  iola features list|enable|disable
516
525
  iola settings list|get|validate|doctor|init
517
- iola gosuslugi terms|consent|configure|status|login|logout|userinfo
526
+ iola gosuslugi terms|consent|status|connect|open|text|screenshot|whoami|debt|notifications|mark-read|logout|configure|login|userinfo
518
527
  iola wiki [open|links]
519
528
  iola context list|show|init
520
529
  iola skills list|show|paths|enable|disable|bundles|bundle|doctor
@@ -2253,11 +2262,16 @@ async function handleGosuslugi(args) {
2253
2262
  const config = await loadConfig();
2254
2263
  const secrets = await loadSecrets();
2255
2264
  const tokens = secrets.gosuslugi?.tokens || null;
2265
+ const browserSession = secrets.gosuslugiBrowser || null;
2256
2266
  const consent = secrets.gosuslugiConsent || null;
2257
2267
  printKeyValue({
2258
- mode: config.gosuslugi?.mode || "personal-local",
2268
+ mode: config.gosuslugi?.mode || "personal-browser",
2259
2269
  enabled: config.gosuslugi?.enabled ? "yes" : "no",
2260
- configured: isGosuslugiConfigured(config) ? "yes" : "no",
2270
+ browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR,
2271
+ browserProfileExists: existsSync(GOSUSLUGI_BROWSER_PROFILE_DIR) ? "yes" : "no",
2272
+ browserConnected: browserSession?.connectedAt ? "yes" : "unknown",
2273
+ browserConnectedAt: browserSession?.connectedAt || "-",
2274
+ oauthConfigured: isGosuslugiConfigured(config) ? "yes" : "no",
2261
2275
  consent: consent?.version === GOSUSLUGI_CONSENT_VERSION ? "accepted" : "not accepted",
2262
2276
  consentAt: consent?.acceptedAt || "-",
2263
2277
  clientId: config.gosuslugi?.clientId ? maskSecret(config.gosuslugi.clientId) : "-",
@@ -2272,6 +2286,61 @@ async function handleGosuslugi(args) {
2272
2286
  return;
2273
2287
  }
2274
2288
 
2289
+ if (action === "connect") {
2290
+ await gosuslugiBrowserConnect(options);
2291
+ return;
2292
+ }
2293
+
2294
+ if (action === "open") {
2295
+ await gosuslugiBrowserOpen(targetOrDefault(rest, options), options);
2296
+ return;
2297
+ }
2298
+
2299
+ if (action === "text") {
2300
+ const result = await gosuslugiBrowserReadText(targetOrDefault(rest, options), options);
2301
+ if (options.output) {
2302
+ await writeFile(path.resolve(options.output), result, "utf8");
2303
+ console.log(`Файл сохранен: ${path.resolve(options.output)}`);
2304
+ } else {
2305
+ console.log(result);
2306
+ }
2307
+ return;
2308
+ }
2309
+
2310
+ if (action === "screenshot") {
2311
+ const outputFile = path.resolve(options.output || "gosuslugi-page.png");
2312
+ await gosuslugiBrowserScreenshot(targetOrDefault(rest, options), outputFile, options);
2313
+ saveArtifact("gosuslugi-screenshot", targetOrDefault(rest, options), outputFile, { url: targetOrDefault(rest, options) });
2314
+ console.log(`Файл сохранен: ${outputFile}`);
2315
+ return;
2316
+ }
2317
+
2318
+ if (action === "whoami" || action === "profile") {
2319
+ const result = await gosuslugiWhoami(options);
2320
+ if (options.json) printJson(result);
2321
+ else printKeyValue(result.summary);
2322
+ return;
2323
+ }
2324
+
2325
+ if (action === "debt" || action === "debts" || action === "payments") {
2326
+ const result = await gosuslugiDebt(options);
2327
+ if (options.json) printJson(result);
2328
+ else printGosuslugiDebt(result);
2329
+ return;
2330
+ }
2331
+
2332
+ if (action === "notifications" || action === "notices") {
2333
+ const result = await gosuslugiNotifications(options);
2334
+ if (options.json) printJson(result);
2335
+ else printGosuslugiNotifications(result);
2336
+ return;
2337
+ }
2338
+
2339
+ if (action === "mark-read") {
2340
+ await gosuslugiMarkNotificationsRead(options);
2341
+ return;
2342
+ }
2343
+
2275
2344
  if (action === "configure") {
2276
2345
  const current = await loadConfig();
2277
2346
  const next = {
@@ -2303,7 +2372,12 @@ async function handleGosuslugi(args) {
2303
2372
  if (action === "logout") {
2304
2373
  const secrets = await loadSecrets();
2305
2374
  delete secrets.gosuslugi;
2375
+ delete secrets.gosuslugiBrowser;
2306
2376
  await saveSecrets(secrets);
2377
+ if (options.profile || options.all) {
2378
+ await rm(GOSUSLUGI_BROWSER_PROFILE_DIR, { recursive: true, force: true }).catch(() => {});
2379
+ console.log("Локальный браузерный профиль Госуслуг удален.");
2380
+ }
2307
2381
  console.log("Локальное подключение Госуслуг удалено.");
2308
2382
  return;
2309
2383
  }
@@ -2315,7 +2389,11 @@ async function handleGosuslugi(args) {
2315
2389
  return;
2316
2390
  }
2317
2391
 
2318
- throw new Error("Команды gosuslugi: terms, consent, configure, status, login, logout, userinfo.");
2392
+ throw new Error("Команды gosuslugi: terms, consent, status, connect, open, text, screenshot, whoami, debt, notifications, mark-read, logout, configure, login, userinfo.");
2393
+ }
2394
+
2395
+ function targetOrDefault(args, options = {}) {
2396
+ return options.url || args.find((item) => !item.startsWith("--")) || GOSUSLUGI_DEFAULT_URL;
2319
2397
  }
2320
2398
 
2321
2399
  async function handleWiki(args) {
@@ -3345,6 +3423,10 @@ async function ensureGosuslugiConsent(options = {}) {
3345
3423
  await acceptGosuslugiConsent(options);
3346
3424
  }
3347
3425
 
3426
+ async function requireGosuslugiConsent() {
3427
+ await ensureGosuslugiConsent();
3428
+ }
3429
+
3348
3430
  function waitForOAuthCallback(settings, expectedState, timeoutMs) {
3349
3431
  const host = settings.redirectHost || "127.0.0.1";
3350
3432
  const port = Number(settings.redirectPort || 18791);
@@ -5960,6 +6042,12 @@ async function aiAsk(args, context = {}) {
5960
6042
  throw new Error('Текст вопроса обязателен. Пример: iola ai ask "Какие школы есть на улице Петрова?"');
5961
6043
  }
5962
6044
 
6045
+ if (!options.bare && isGosuslugiPersonalIntent(question)) {
6046
+ const answer = await answerGosuslugiQuestion(question, options);
6047
+ if (!options.quiet) console.log(answer);
6048
+ return answer;
6049
+ }
6050
+
5963
6051
  const config = await loadConfig();
5964
6052
  const providerConfig = await resolveUsableAiProfile(config, options);
5965
6053
  if (providerConfig.provider === "codex") await assertPermission("codex");
@@ -6128,7 +6216,7 @@ async function buildLocalToolPlan(question, providerConfig, options) {
6128
6216
  "Ты планировщик CLI iola. Верни только JSON.",
6129
6217
  `Доступные tools: ${availableToolNames(options).join(", ")}.`,
6130
6218
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
6131
- "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
6219
+ "Минимальные 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}.",
6132
6220
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
6133
6221
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
6134
6222
  `Вопрос: ${question}`,
@@ -6158,6 +6246,12 @@ function inferToolPlan(question, options = {}) {
6158
6246
  const steps = [];
6159
6247
  if (normalized.includes("без телефона")) {
6160
6248
  steps.push({ tool: "export_report", args: { name: "missing-phones" } });
6249
+ } else if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
6250
+ steps.push({ tool: "gosuslugi_notifications", args: { unread: /непрочитан|нов/iu.test(normalized), limit: 15 } });
6251
+ } else if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
6252
+ steps.push({ tool: "gosuslugi_debt", args: {} });
6253
+ } else if (/(фио|дата рождения|профиль|кто я)/iu.test(normalized) && /госуслуг/iu.test(normalized)) {
6254
+ steps.push({ tool: "gosuslugi_whoami", args: {} });
6161
6255
  } else {
6162
6256
  const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
6163
6257
  steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
@@ -6247,6 +6341,18 @@ async function executeToolPlan(plan, options = {}) {
6247
6341
  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" });
6248
6342
  current = [{ url: step.args?.url, text }];
6249
6343
  outputs.push({ tool: step.tool, rows: 1 });
6344
+ } else if (step.tool === "gosuslugi_whoami") {
6345
+ const result = await gosuslugiWhoami(step.args || {});
6346
+ current = [result.summary];
6347
+ outputs.push({ tool: step.tool, rows: 1 });
6348
+ } else if (step.tool === "gosuslugi_debt") {
6349
+ const result = await gosuslugiDebt(step.args || {});
6350
+ current = [{ total: result.total, amount: result.amount, debts: result.debts }];
6351
+ outputs.push({ tool: step.tool, rows: result.debts.length });
6352
+ } else if (step.tool === "gosuslugi_notifications") {
6353
+ const result = await gosuslugiNotifications(step.args || {});
6354
+ current = [{ total: result.total, unread: result.unread, items: result.items }];
6355
+ outputs.push({ tool: step.tool, rows: result.items.length });
6250
6356
  } else if (String(step.tool || "").startsWith("mcp:")) {
6251
6357
  const result = await callConfiguredMcpTool(step.tool, step.args || {});
6252
6358
  current = Array.isArray(result) ? result : [result];
@@ -6985,7 +7091,12 @@ async function onboard(args = []) {
6985
7091
  if (components.includes("gosuslugi")) {
6986
7092
  if (process.stdin.isTTY) await handleGosuslugi(["consent"]);
6987
7093
  else await handleGosuslugi(["terms"]);
6988
- console.log("Параметры подключения можно указать командой: iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid");
7094
+ await ensureBrowserRuntimeForGosuslugi();
7095
+ if (process.stdin.isTTY && await confirm("Открыть Госуслуги для входа сейчас? [Y/n] ")) {
7096
+ await gosuslugiBrowserConnect({ yes: true });
7097
+ } else {
7098
+ console.log("Подключить личные Госуслуги позже: iola gosuslugi connect");
7099
+ }
6989
7100
  }
6990
7101
  if (components.includes("index")) {
6991
7102
  await setFilesMode("read-only", await loadConfig());
@@ -7039,7 +7150,7 @@ function parseOptions(args) {
7039
7150
 
7040
7151
  for (let index = 0; index < args.length; index += 1) {
7041
7152
  const arg = args[index];
7042
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
7153
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
7043
7154
  result[arg.slice(2)] = true;
7044
7155
  } else if (arg === "--check" || arg === "--upgrade-node") {
7045
7156
  result.check = true;
@@ -7280,7 +7391,8 @@ async function buildSkillsText(config, question = "", options = {}) {
7280
7391
  const chunks = [];
7281
7392
  const selected = selectSkillsForPrompt(config, question, options);
7282
7393
  for (const skill of listSkills(config)) {
7283
- if (!skill.enabled || !selected.has(skill.name)) continue;
7394
+ const active = skill.enabled || (skill.name === "gosuslugi" && config.gosuslugi?.enabled);
7395
+ if (!active || !selected.has(skill.name)) continue;
7284
7396
  const text = await readFile(skill.file, "utf8");
7285
7397
  chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
7286
7398
  }
@@ -7296,6 +7408,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
7296
7408
  if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
7297
7409
  if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
7298
7410
  if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
7411
+ if (enabled.has("gosuslugi") && /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized)) selected.add("gosuslugi");
7299
7412
  return selected;
7300
7413
  }
7301
7414
 
@@ -7481,6 +7594,10 @@ async function getBrowserStatus() {
7481
7594
  }
7482
7595
 
7483
7596
  async function installBrowserRuntime() {
7597
+ if (existsSync(BROWSER_RUNTIME_PACKAGE)) {
7598
+ console.log(`Browser runtime уже установлен: ${BROWSER_RUNTIME_DIR}`);
7599
+ return;
7600
+ }
7484
7601
  await mkdir(BROWSER_RUNTIME_DIR, { recursive: true });
7485
7602
  const packageFile = path.join(BROWSER_RUNTIME_DIR, "package.json");
7486
7603
  if (!existsSync(packageFile)) {
@@ -7515,6 +7632,407 @@ async function runBrowserAutomation(action, params) {
7515
7632
  }
7516
7633
  }
7517
7634
 
7635
+ async function ensureBrowserRuntimeForGosuslugi() {
7636
+ if (existsSync(BROWSER_RUNTIME_PACKAGE)) return;
7637
+ console.log("Browser runtime не установлен. Устанавливаю Playwright/Chromium для локального браузерного профиля.");
7638
+ await installBrowserRuntime();
7639
+ }
7640
+
7641
+ async function gosuslugiBrowserConnect(options = {}) {
7642
+ await ensureGosuslugiConsent({ yes: options.yes });
7643
+ await ensureBrowserRuntimeForGosuslugi();
7644
+ await saveConfig({ gosuslugi: { ...(await loadConfig()).gosuslugi, enabled: true, mode: "personal-browser" } });
7645
+ const url = options.url || GOSUSLUGI_DEFAULT_URL;
7646
+ console.log(`Открываю Госуслуги в отдельном локальном профиле: ${GOSUSLUGI_BROWSER_PROFILE_DIR}`);
7647
+ console.log("Авторизуйтесь в открывшемся окне. Когда закончите, закройте окно браузера.");
7648
+ await runPersistentBrowserAutomation("open", {
7649
+ url,
7650
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7651
+ headed: true,
7652
+ waitMs: Number(options.wait || 0),
7653
+ timeout: Number(options.timeout || 120000),
7654
+ viewport: options.viewport || "1366x768",
7655
+ });
7656
+ const secrets = await loadSecrets();
7657
+ secrets.gosuslugiBrowser = {
7658
+ mode: "personal-browser",
7659
+ profileDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7660
+ connectedAt: new Date().toISOString(),
7661
+ lastUrl: url,
7662
+ };
7663
+ await saveSecrets(secrets);
7664
+ console.log("Локальный браузерный профиль Госуслуг сохранен.");
7665
+ }
7666
+
7667
+ async function gosuslugiBrowserOpen(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
7668
+ await requireGosuslugiConsent();
7669
+ await ensureBrowserRuntimeForGosuslugi();
7670
+ await runPersistentBrowserAutomation("open", {
7671
+ url,
7672
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7673
+ headed: true,
7674
+ waitMs: Number(options.wait || 0),
7675
+ timeout: Number(options.timeout || 120000),
7676
+ viewport: options.viewport || "1366x768",
7677
+ });
7678
+ }
7679
+
7680
+ async function gosuslugiBrowserReadText(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
7681
+ await requireGosuslugiConsent();
7682
+ await ensureBrowserRuntimeForGosuslugi();
7683
+ return runPersistentBrowserAutomation("text", {
7684
+ url,
7685
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7686
+ headed: Boolean(options.headed),
7687
+ waitMs: Number(options.wait || 3000),
7688
+ timeout: Number(options.timeout || 60000),
7689
+ viewport: options.viewport || "1366x768",
7690
+ });
7691
+ }
7692
+
7693
+ async function gosuslugiBrowserScreenshot(url = GOSUSLUGI_DEFAULT_URL, outputFile, options = {}) {
7694
+ await requireGosuslugiConsent();
7695
+ await ensureBrowserRuntimeForGosuslugi();
7696
+ await runPersistentBrowserAutomation("screenshot", {
7697
+ url,
7698
+ output: outputFile,
7699
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7700
+ headed: Boolean(options.headed),
7701
+ waitMs: Number(options.wait || 3000),
7702
+ timeout: Number(options.timeout || 60000),
7703
+ viewport: options.viewport || "1366x768",
7704
+ });
7705
+ }
7706
+
7707
+ async function gosuslugiWhoami(options = {}) {
7708
+ const data = await gosuslugiBrowserApiJson({
7709
+ pageUrl: "https://lk.gosuslugi.ru/settings/account",
7710
+ endpoint: "https://www.gosuslugi.ru/api/lk/v1/users/data",
7711
+ waitMs: Number(options.wait || 3000),
7712
+ });
7713
+ const person = data.person?.person || data.person || data;
7714
+ const summary = {
7715
+ fio: [data.lastName || person.lastName, data.firstName || person.firstName, data.middleName || person.middleName].filter(Boolean).join(" ") || data.formattedName || "-",
7716
+ birthDate: person.birthDate || data.birthDate || "-",
7717
+ status: data.assuranceLevel === "AL20" || person.trusted ? "Подтвержденная учетная запись" : data.assuranceLevel || "-",
7718
+ phone: options.full ? (data.personMobilePhone || data.mobile || "-") : maskPhone(data.personMobilePhone || data.mobile || ""),
7719
+ email: options.full ? (data.personEMail || data.personEmail || data.email || "-") : maskEmail(data.personEMail || data.personEmail || data.email || ""),
7720
+ snils: options.full ? (person.snils || data.personSnils || data.snils || "-") : maskDocument(person.snils || data.personSnils || data.snils || ""),
7721
+ inn: options.full ? (person.inn || data.personINN || data.inn || "-") : maskDocument(person.inn || data.personINN || data.inn || ""),
7722
+ };
7723
+ return {
7724
+ summary,
7725
+ raw: options.full ? redactGosuslugiSensitive(data, { keepPersonal: true }) : undefined,
7726
+ };
7727
+ }
7728
+
7729
+ async function gosuslugiDebt(options = {}) {
7730
+ const data = await gosuslugiBrowserApiJson({
7731
+ pageUrl: "https://www.gosuslugi.ru/pay/forPayment",
7732
+ endpoint: "https://www.gosuslugi.ru/api/pay/v2/informer/fetch",
7733
+ waitMs: Number(options.wait || 5000),
7734
+ });
7735
+ const groups = Array.isArray(data.groups) ? data.groups : [];
7736
+ const debts = groups.flatMap((group) => (group.bills || []).map((bill) => ({
7737
+ group: group.name || group.code || "-",
7738
+ caption: bill.caption || "-",
7739
+ amount: Number(bill.amount || 0),
7740
+ billDate: bill.billDate || "-",
7741
+ supplier: bill.supplierFullName || "-",
7742
+ document: bill.document?.typeName ? `${bill.document.typeName} ${bill.document.number || ""}`.trim() : "-",
7743
+ })));
7744
+ return {
7745
+ total: Number(data.summary?.total || debts.length || 0),
7746
+ amount: Number(data.summary?.amount || debts.reduce((sum, item) => sum + item.amount, 0)),
7747
+ groups: groups.map((group) => ({ name: group.name, code: group.code, total: group.summary?.total || 0, amount: group.summary?.amount || 0 })),
7748
+ debts,
7749
+ };
7750
+ }
7751
+
7752
+ async function gosuslugiNotifications(options = {}) {
7753
+ 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";
7754
+ const pageSize = Number(options.limit || 15);
7755
+ const unread = options.unread ? "true" : "false";
7756
+ const counters = await gosuslugiBrowserApiJson({
7757
+ pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
7758
+ endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/counters?types=${types},PARTNERS&isArchive=false`,
7759
+ waitMs: Number(options.wait || 3000),
7760
+ });
7761
+ const feed = await gosuslugiBrowserApiJson({
7762
+ pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
7763
+ endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/?unread=${unread}&isArchive=false&isHide=false&types=${types}&pageSize=${pageSize}&status=&startDate=&lastFeedId=&lastFeedDate=&q=`,
7764
+ waitMs: Number(options.wait || 3000),
7765
+ });
7766
+ const items = (feed.items || []).map((item) => ({
7767
+ id: item.id,
7768
+ unread: Boolean(item.unread),
7769
+ date: item.date || "-",
7770
+ type: item.feedType || "-",
7771
+ title: item.title || "-",
7772
+ subtitle: item.subTitle || "-",
7773
+ status: item.status || "-",
7774
+ summary: summarizeNotificationData(item.data),
7775
+ }));
7776
+ return {
7777
+ total: counters.total || feed.items?.length || 0,
7778
+ unread: counters.unread || items.filter((item) => item.unread).length,
7779
+ counters: counters.counter || [],
7780
+ hasMore: Boolean(feed.hasMore),
7781
+ items,
7782
+ };
7783
+ }
7784
+
7785
+ async function gosuslugiMarkNotificationsRead(options = {}) {
7786
+ await requireGosuslugiConsent();
7787
+ if (!options.yes) {
7788
+ const ok = await confirm("Отметить уведомления Госуслуг прочитанными? Это изменит состояние личного кабинета. [y/N] ");
7789
+ if (!ok) {
7790
+ console.log("Операция отменена.");
7791
+ return;
7792
+ }
7793
+ }
7794
+ await gosuslugiBrowserClickText({
7795
+ 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",
7796
+ text: "Прочитать все",
7797
+ waitMs: Number(options.wait || 5000),
7798
+ });
7799
+ console.log("Команда отметки прочитанным выполнена. Проверьте статус: iola gosuslugi notifications --unread");
7800
+ }
7801
+
7802
+ function printGosuslugiDebt(result) {
7803
+ printKeyValue({
7804
+ total: result.total,
7805
+ amount: `${formatRub(result.amount)} Р`,
7806
+ });
7807
+ if (!result.debts.length) {
7808
+ console.log("Задолженности не найдены.");
7809
+ return;
7810
+ }
7811
+ printTable(result.debts.map((item) => ({
7812
+ group: item.group,
7813
+ amount: `${formatRub(item.amount)} Р`,
7814
+ date: item.billDate,
7815
+ caption: item.caption,
7816
+ })), [
7817
+ ["group", "Группа"],
7818
+ ["amount", "Сумма"],
7819
+ ["date", "Дата"],
7820
+ ["caption", "Описание"],
7821
+ ]);
7822
+ }
7823
+
7824
+ function printGosuslugiNotifications(result) {
7825
+ printKeyValue({ total: result.total, unread: result.unread, hasMore: result.hasMore ? "yes" : "no" });
7826
+ printTable(result.items.map((item) => ({
7827
+ unread: item.unread ? "new" : "read",
7828
+ date: item.date,
7829
+ type: item.type,
7830
+ title: item.title,
7831
+ subtitle: item.subtitle,
7832
+ summary: item.summary,
7833
+ })), [
7834
+ ["unread", "Статус"],
7835
+ ["date", "Дата"],
7836
+ ["type", "Тип"],
7837
+ ["title", "Заголовок"],
7838
+ ["subtitle", "Подзаголовок"],
7839
+ ["summary", "Детали"],
7840
+ ]);
7841
+ }
7842
+
7843
+ function summarizeNotificationData(data) {
7844
+ if (!data || typeof data !== "object") return "";
7845
+ const snippets = Array.isArray(data.snippets) ? data.snippets : [];
7846
+ if (snippets.length) {
7847
+ const first = snippets[0];
7848
+ return [first.orgName, first.address, first.date].filter(Boolean).join(" | ");
7849
+ }
7850
+ return [data.messageType, data.messageUuid, data.orderId, data.passCodeEpguCode].filter(Boolean).join(" | ");
7851
+ }
7852
+
7853
+ function formatRub(value) {
7854
+ return Number(value || 0).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
7855
+ }
7856
+
7857
+ function isGosuslugiPersonalIntent(question) {
7858
+ const normalized = String(question || "").toLocaleLowerCase("ru-RU");
7859
+ return /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized);
7860
+ }
7861
+
7862
+ async function answerGosuslugiQuestion(question, options = {}) {
7863
+ const normalized = String(question || "").toLocaleLowerCase("ru-RU");
7864
+ if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
7865
+ const result = await gosuslugiNotifications({ unread: /непрочитан|нов/iu.test(normalized), limit: options.limit || 10 });
7866
+ const lines = [`На Госуслугах: всего уведомлений ${result.total}, непрочитанных ${result.unread}.`];
7867
+ const items = result.items.slice(0, Number(options.limit || 5));
7868
+ if (items.length) {
7869
+ lines.push("");
7870
+ for (const item of items) {
7871
+ lines.push(`- ${item.unread ? "новое" : "прочитано"}: ${item.title} — ${item.subtitle} (${item.date})`);
7872
+ }
7873
+ }
7874
+ return lines.join("\n");
7875
+ }
7876
+ if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
7877
+ const result = await gosuslugiDebt(options);
7878
+ if (!result.debts.length) return "На Госуслугах задолженности к оплате не найдены.";
7879
+ const lines = [`На Госуслугах найдено задолженностей: ${result.total}. Общая сумма: ${formatRub(result.amount)} Р.`];
7880
+ for (const item of result.debts) {
7881
+ lines.push(`- ${item.group}: ${formatRub(item.amount)} Р — ${item.caption}`);
7882
+ }
7883
+ return lines.join("\n");
7884
+ }
7885
+ const result = await gosuslugiWhoami(options);
7886
+ return [
7887
+ `ФИО: ${result.summary.fio}`,
7888
+ `Дата рождения: ${result.summary.birthDate}`,
7889
+ `Статус: ${result.summary.status}`,
7890
+ ].join("\n");
7891
+ }
7892
+
7893
+ function maskPhone(value) {
7894
+ const text = String(value || "");
7895
+ return text.replace(/(\+?\d)([\d\s()-]{4,})(\d{2})$/u, "$1***$3") || "-";
7896
+ }
7897
+
7898
+ function maskEmail(value) {
7899
+ const text = String(value || "");
7900
+ const [name, domain] = text.split("@");
7901
+ if (!name || !domain) return text || "-";
7902
+ return `${name.slice(0, 2)}***@${domain}`;
7903
+ }
7904
+
7905
+ function maskDocument(value) {
7906
+ const digits = String(value || "").replace(/\D+/g, "");
7907
+ if (!digits) return "-";
7908
+ return `***${digits.slice(-4)}`;
7909
+ }
7910
+
7911
+ function redactGosuslugiSensitive(value, options = {}) {
7912
+ if (Array.isArray(value)) return value.map((item) => redactGosuslugiSensitive(item, options));
7913
+ if (!value || typeof value !== "object") return value;
7914
+ const result = {};
7915
+ for (const [key, item] of Object.entries(value)) {
7916
+ if (/token|cookie|session|password|secret|jwt|auth/i.test(key)) result[key] = "[redacted]";
7917
+ else if (!options.keepPersonal && /(snils|inn|passport|number|series|address|mobile|email|phone)/i.test(key)) result[key] = "[redacted]";
7918
+ else result[key] = redactGosuslugiSensitive(item, options);
7919
+ }
7920
+ return result;
7921
+ }
7922
+
7923
+ async function runPersistentBrowserAutomation(action, params) {
7924
+ await ensureBrowserRuntime();
7925
+ await mkdir(params.userDataDir, { recursive: true });
7926
+ const releaseLock = params.userDataDir === GOSUSLUGI_BROWSER_PROFILE_DIR ? await acquireDirectoryLock(GOSUSLUGI_BROWSER_LOCK_DIR, 180000) : async () => {};
7927
+ const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-profile-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
7928
+ await writeFile(scriptFile, persistentBrowserAutomationScript(action, params), "utf8");
7929
+ try {
7930
+ const options = action === "open" ? { cwd: BROWSER_RUNTIME_DIR, inherit: true } : { cwd: BROWSER_RUNTIME_DIR };
7931
+ const result = await runCommand(process.execPath, [scriptFile], options);
7932
+ return result.stdout?.trim() || "";
7933
+ } finally {
7934
+ await rm(scriptFile, { force: true }).catch(() => {});
7935
+ await releaseLock();
7936
+ }
7937
+ }
7938
+
7939
+ async function acquireDirectoryLock(lockDir, timeoutMs = 60000) {
7940
+ const started = Date.now();
7941
+ while (true) {
7942
+ try {
7943
+ await mkdir(lockDir, { recursive: false });
7944
+ await writeFile(path.join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2), "utf8").catch(() => {});
7945
+ return async () => {
7946
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {});
7947
+ };
7948
+ } catch {
7949
+ if (Date.now() - started > timeoutMs) {
7950
+ throw new Error("Браузерный профиль Госуслуг занят другим процессом. Закройте окно Госуслуг или повторите команду позже.");
7951
+ }
7952
+ await sleep(1000);
7953
+ }
7954
+ }
7955
+ }
7956
+
7957
+ async function gosuslugiBrowserApiJson(params) {
7958
+ await requireGosuslugiConsent();
7959
+ await ensureBrowserRuntimeForGosuslugi();
7960
+ const raw = await runPersistentBrowserAutomation("api-json", {
7961
+ pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
7962
+ endpoint: params.endpoint,
7963
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7964
+ headed: params.headed !== false,
7965
+ waitMs: Number(params.waitMs || 0),
7966
+ timeout: Number(params.timeout || 60000),
7967
+ viewport: params.viewport || "1366x768",
7968
+ });
7969
+ return JSON.parse(raw);
7970
+ }
7971
+
7972
+ async function gosuslugiBrowserClickText(params) {
7973
+ await requireGosuslugiConsent();
7974
+ await ensureBrowserRuntimeForGosuslugi();
7975
+ return runPersistentBrowserAutomation("click-text", {
7976
+ pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
7977
+ text: params.text,
7978
+ userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
7979
+ headed: true,
7980
+ waitMs: Number(params.waitMs || 3000),
7981
+ timeout: Number(params.timeout || 60000),
7982
+ viewport: params.viewport || "1366x768",
7983
+ });
7984
+ }
7985
+
7986
+ function persistentBrowserAutomationScript(action, params) {
7987
+ return `
7988
+ import { chromium } from "playwright";
7989
+ const action = ${JSON.stringify(action)};
7990
+ const params = ${JSON.stringify(params)};
7991
+ const [width, height] = String(params.viewport || "1366x768").split("x").map(Number);
7992
+ const context = await chromium.launchPersistentContext(params.userDataDir, {
7993
+ headless: !params.headed,
7994
+ viewport: { width: width || 1366, height: height || 768 },
7995
+ });
7996
+ context.setDefaultTimeout(params.timeout || 60000);
7997
+ const page = context.pages()[0] || await context.newPage();
7998
+ try {
7999
+ await page.goto(params.url || params.pageUrl, { waitUntil: "domcontentloaded", timeout: params.timeout || 60000 });
8000
+ if (params.waitMs) await page.waitForTimeout(params.waitMs);
8001
+ if (action === "open") {
8002
+ if (params.headed) {
8003
+ page.on("close", async () => {
8004
+ await context.close().catch(() => {});
8005
+ });
8006
+ while (!page.isClosed()) {
8007
+ await page.waitForTimeout(1000).catch(() => {});
8008
+ }
8009
+ }
8010
+ } else if (action === "text") {
8011
+ console.log((await page.locator("body").innerText()).trim());
8012
+ } else if (action === "screenshot") {
8013
+ await page.screenshot({ path: params.output, fullPage: true });
8014
+ } else if (action === "api-json") {
8015
+ const data = await page.evaluate(async (endpoint) => {
8016
+ const response = await fetch(endpoint, {
8017
+ credentials: "include",
8018
+ headers: { accept: "application/json" },
8019
+ });
8020
+ const text = await response.text();
8021
+ if (!response.ok) throw new Error(response.status + " " + response.statusText + ": " + text.slice(0, 500));
8022
+ return JSON.parse(text);
8023
+ }, params.endpoint);
8024
+ console.log(JSON.stringify(data));
8025
+ } else if (action === "click-text") {
8026
+ await page.getByText(params.text, { exact: true }).first().click();
8027
+ if (params.waitMs) await page.waitForTimeout(params.waitMs);
8028
+ console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
8029
+ }
8030
+ } finally {
8031
+ await context.close().catch(() => {});
8032
+ }
8033
+ `;
8034
+ }
8035
+
7518
8036
  function browserAutomationScript(action, params) {
7519
8037
  return `
7520
8038
  import { chromium } from "playwright";
@@ -7745,6 +8263,9 @@ function mcpTools() {
7745
8263
  { name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
7746
8264
  { name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
7747
8265
  { name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
8266
+ { name: "gosuslugi.whoami", description: "Прочитать ФИО и дату рождения из личного профиля Госуслуг через локальный браузерный профиль.", inputSchema: schema({ full: { type: "boolean" } }) },
8267
+ { name: "gosuslugi.debt", description: "Прочитать задолженности и платежи к оплате на Госуслугах.", inputSchema: schema() },
8268
+ { name: "gosuslugi.notifications", description: "Прочитать уведомления Госуслуг.", inputSchema: schema({ unread: { type: "boolean" }, limit: { type: "number" } }) },
7748
8269
  ];
7749
8270
  }
7750
8271
 
@@ -7782,6 +8303,9 @@ async function callMcpTool(name, args = {}) {
7782
8303
  await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
7783
8304
  return { output };
7784
8305
  }
8306
+ if (name === "gosuslugi.whoami") return gosuslugiWhoami(args);
8307
+ if (name === "gosuslugi.debt") return gosuslugiDebt(args);
8308
+ if (name === "gosuslugi.notifications") return gosuslugiNotifications(args);
7785
8309
  return executeRpc(name, { ...args, _: [] });
7786
8310
  }
7787
8311
 
@@ -8947,7 +9471,9 @@ function validateConfig(config) {
8947
9471
  if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
8948
9472
  }
8949
9473
  if (config.gosuslugi?.enabled && !isGosuslugiConfigured(config)) {
8950
- errors.push("gosuslugi включен, но authUrl/tokenUrl/clientId не заполнены");
9474
+ if ((config.gosuslugi?.mode || "personal-browser") !== "personal-browser") {
9475
+ errors.push("gosuslugi включен в OAuth/OIDC-режиме, но authUrl/tokenUrl/clientId не заполнены");
9476
+ }
8951
9477
  }
8952
9478
  return errors;
8953
9479
  }
@@ -8958,7 +9484,7 @@ function configSchema() {
8958
9484
  required: ["api", "ai"],
8959
9485
  properties: {
8960
9486
  api: { required: ["baseUrl", "mcpBaseUrl"] },
8961
- gosuslugi: { requiredWhenEnabled: ["authUrl", "tokenUrl", "clientId"], optional: ["userinfoUrl", "clientSecret", "scope", "redirectHost", "redirectPort", "redirectPath"] },
9487
+ gosuslugi: { modes: ["personal-browser", "personal-local"], browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR, oauthRequiredWhenEnabled: ["authUrl", "tokenUrl", "clientId"], optional: ["userinfoUrl", "clientSecret", "scope", "redirectHost", "redirectPort", "redirectPath"] },
8962
9488
  ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
8963
9489
  permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
8964
9490
  toolsets: { available: Object.keys(TOOLSETS) },
@@ -9016,6 +9542,10 @@ function setConfigValue(config, key, value) {
9016
9542
  current[parts.at(-1)] = value;
9017
9543
  }
9018
9544
 
9545
+ function sleep(ms) {
9546
+ return new Promise((resolve) => setTimeout(resolve, ms));
9547
+ }
9548
+
9019
9549
  async function loadSecrets() {
9020
9550
  try {
9021
9551
  return JSON.parse(await readFile(SECRETS_FILE, "utf8"));
@@ -97,6 +97,15 @@ iola settings validate
97
97
  iola gosuslugi status
98
98
  iola gosuslugi terms
99
99
  iola gosuslugi consent
100
+ iola gosuslugi connect
101
+ iola gosuslugi open
102
+ iola gosuslugi text https://www.gosuslugi.ru/
103
+ iola gosuslugi screenshot https://www.gosuslugi.ru/ --output gosuslugi.png
104
+ iola gosuslugi whoami
105
+ iola gosuslugi debt
106
+ iola gosuslugi notifications --unread
107
+ iola gosuslugi mark-read
108
+ iola gosuslugi logout --all
100
109
  iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid
101
110
  iola gosuslugi login
102
111
  iola gosuslugi userinfo --json
@@ -1,89 +1,103 @@
1
1
  # Личное подключение Госуслуг
2
2
 
3
- `iola-cli` поддерживает локальное подключение личной учетной записи Госуслуг на компьютере пользователя.
3
+ `iola-cli` подключает личный аккаунт Госуслуг через отдельный локальный браузерный профиль на компьютере пользователя.
4
4
 
5
- Сценарий рассчитан именно на пользователя, который ставит CLI на свой ПК и подключает свою учетную запись. Ключи организации или администрации в публичный пакет не вшиваются.
5
+ Это не официальный API ЕСИА и не встраивание ключей организации. Пользователь сам открывает окно браузера, сам вводит логин, пароль и код подтверждения. CLI сохраняет только локальный браузерный профиль в домашней папке пользователя.
6
6
 
7
- ## Согласие пользователя
7
+ ## Первый вход
8
8
 
9
- Перед входом CLI показывает текст согласия в терминале. В wiki он не дублируется, чтобы пользователь видел актуальную формулировку именно в установленной версии CLI.
9
+ ```bash
10
+ iola gosuslugi connect
11
+ ```
12
+
13
+ Что происходит:
14
+
15
+ 1. CLI показывает согласие пользователя.
16
+ 2. Устанавливает browser runtime, если он еще не установлен.
17
+ 3. Открывает Госуслуги в отдельном профиле Chromium.
18
+ 4. Пользователь сам проходит вход.
19
+ 5. После завершения пользователь закрывает окно браузера.
20
+ 6. CLI сохраняет локальное состояние профиля.
10
21
 
11
- Посмотреть текст:
22
+ Профиль хранится в:
23
+
24
+ ```text
25
+ ~/.iola/gosuslugi-browser-profile
26
+ ```
27
+
28
+ ## Работа с открытым профилем
29
+
30
+ Открыть Госуслуги:
12
31
 
13
32
  ```bash
14
- iola gosuslugi terms
33
+ iola gosuslugi open
15
34
  ```
16
35
 
17
- Принять заранее:
36
+ Прочитать видимый текст страницы:
18
37
 
19
38
  ```bash
20
- iola gosuslugi consent
39
+ iola gosuslugi text https://www.gosuslugi.ru/
21
40
  ```
22
41
 
23
- ## Настройка
42
+ Сделать скриншот:
24
43
 
25
- Для работы нужны параметры пользовательского OAuth/OIDC-подключения:
44
+ ```bash
45
+ iola gosuslugi screenshot https://www.gosuslugi.ru/ --output gosuslugi.png
46
+ ```
26
47
 
27
- - authorization endpoint;
28
- - token endpoint;
29
- - client ID;
30
- - разрешенный redirect URI;
31
- - scope;
32
- - optional client secret, если он выдан именно пользователю или локальному приложению;
33
- - optional userinfo endpoint.
48
+ ## Read-only данные
34
49
 
35
- Команда настройки:
50
+ Краткие данные профиля:
36
51
 
37
52
  ```bash
38
- iola gosuslugi configure \
39
- --auth-url "https://..." \
40
- --token-url "https://..." \
41
- --userinfo-url "https://..." \
42
- --client-id "CLIENT_ID" \
43
- --scope "openid" \
44
- --redirect-port 18791
53
+ iola gosuslugi whoami
45
54
  ```
46
55
 
47
- CLI покажет redirect URI:
56
+ Задолженности и платежи к оплате:
48
57
 
49
- ```text
50
- http://127.0.0.1:18791/gosuslugi/callback
58
+ ```bash
59
+ iola gosuslugi debt
51
60
  ```
52
61
 
53
- Этот URI должен быть разрешен в настройках подключения.
54
-
55
- ## Вход
62
+ Уведомления:
56
63
 
57
64
  ```bash
58
- iola gosuslugi login
65
+ iola gosuslugi notifications
66
+ iola gosuslugi notifications --unread
59
67
  ```
60
68
 
61
- Что происходит:
69
+ Отметить уведомления прочитанными можно только отдельной командой с подтверждением:
62
70
 
63
- 1. CLI запускает локальный callback server на `127.0.0.1`.
64
- 2. Показывается и сохраняется согласие пользователя.
65
- 3. Открывается экран входа Госуслуг.
66
- 4. Пользователь сам вводит логин, пароль, SMS/2FA.
67
- 5. Госуслуги возвращают `authorization code` на локальный callback.
68
- 6. CLI обменивает code на токены и сохраняет их локально.
71
+ ```bash
72
+ iola gosuslugi mark-read
73
+ ```
69
74
 
70
- Токены хранятся в `~/.iola/secrets.json` на компьютере пользователя.
75
+ AI-агент может использовать read-only tools Госуслуг, если пользователь спрашивает про ФИО, дату рождения, задолженности, штрафы, налоги, платежи или уведомления.
71
76
 
72
- ## Проверка
77
+ ## Статус
73
78
 
74
79
  ```bash
75
80
  iola gosuslugi status
76
- iola gosuslugi userinfo --json
77
81
  ```
78
82
 
79
- ## Выход
83
+ Команда показывает, принято ли согласие, где лежит локальный профиль и когда он был создан.
84
+
85
+ ## Отключение
86
+
87
+ Удалить локальное подключение:
80
88
 
81
89
  ```bash
82
- iola gosuslugi logout
90
+ iola gosuslugi logout --all
83
91
  ```
84
92
 
85
- Команда удаляет локально сохраненные токены.
93
+ `--all` удаляет сохраненный браузерный профиль.
86
94
 
87
95
  ## Ограничения
88
96
 
89
- Без параметров пользовательского подключения команда `login` не сможет завершить подключение. Все ключи, токены и настройки хранятся только локально у пользователя.
97
+ - CLI работает только с тем, что доступно пользователю в локальном браузере.
98
+ - CLI не извлекает cookies, session tokens или внутренние токены Госуслуг.
99
+ - Юридически значимые действия должны требовать отдельного подтверждения пользователя.
100
+ - Сессия браузерного профиля может протухнуть. Если Госуслуги попросят повторный вход или 2FA, выполните `iola gosuslugi connect`.
101
+ - Если Госуслуги попросят повторный вход, код или подтверждение, пользователь проходит его сам в открытом окне.
102
+
103
+ OAuth/OIDC-команды `configure`, `login`, `userinfo` оставлены для случая, если у пользователя есть официально зарегистрированное подключение ЕСИА.