@iola_adm/iola-cli 0.1.63 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -12
- package/package.json +1 -1
- package/src/cli.js +68 -1010
- package/wiki/Daemon-RPC-/320/270-cron.md +0 -7
- package/wiki/Home.md +1 -1
- package/wiki//320/220/321/200/321/205/320/270/320/262/321/213-/320/270-/320/274/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +3 -1
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +0 -20
- package/wiki//320/234/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +69 -0
- package/wiki//320/237/320/265/321/200/320/262/321/213/320/271-/320/267/320/260/320/277/321/203/321/201/320/272.md +3 -2
- package/skills/gosuslugi/SKILL.md +0 -16
- package/wiki//320/237/320/276/320/264/320/272/320/273/321/216/321/207/320/265/320/275/320/270/320/265-/320/223/320/276/321/201/321/203/321/201/320/273/321/203/320/263.md +0 -138
package/src/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
4
3
|
import { createServer } from "node:http";
|
|
5
4
|
import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -26,31 +25,14 @@ const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
|
|
|
26
25
|
const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
|
|
27
26
|
const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
|
|
28
27
|
const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
|
|
29
|
-
const GOSUSLUGI_BROWSER_PROFILE_DIR = path.join(CONFIG_DIR, "gosuslugi-browser-profile");
|
|
30
|
-
const GOSUSLUGI_BROWSER_LOCK_DIR = path.join(CONFIG_DIR, "gosuslugi-browser-profile.lock");
|
|
31
|
-
const GOSUSLUGI_DEFAULT_URL = "https://www.gosuslugi.ru/";
|
|
32
28
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
33
|
-
const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"
|
|
29
|
+
const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
|
|
34
30
|
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
35
31
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
36
32
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
37
33
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
38
34
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
39
35
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
40
|
-
const GOSUSLUGI_CONSENT_VERSION = "2026-05-26-personal-local-v1";
|
|
41
|
-
const GOSUSLUGI_CONSENT_TEXT = `Подключение личных Госуслуг
|
|
42
|
-
|
|
43
|
-
Вы подключаете личную учетную запись Госуслуг к локальному CLI-агенту iola-cli на этом компьютере.
|
|
44
|
-
|
|
45
|
-
Нажимая "Да", вы подтверждаете, что:
|
|
46
|
-
- используете собственную учетную запись Госуслуг;
|
|
47
|
-
- понимаете, что все действия, выполненные через CLI-агента после подключения, считаются действиями владельца этой учетной записи;
|
|
48
|
-
- разрешаете iola-cli локально сохранить данные доступа, необходимые для повторного входа или выполнения запросов от вашего имени;
|
|
49
|
-
- понимаете, что данные доступа хранятся только на этом компьютере в локальном хранилище пользователя и не передаются разработчикам CLI, администрации города или третьим лицам;
|
|
50
|
-
- обязуетесь не подключать чужие учетные записи и не передавать локальные файлы доступа другим лицам;
|
|
51
|
-
- понимаете, что перед юридически значимыми действиями, отправкой заявлений, оплатой, подписанием или изменением персональных данных CLI должен запросить отдельное подтверждение.
|
|
52
|
-
|
|
53
|
-
Продолжить подключение?`;
|
|
54
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
55
37
|
const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
56
38
|
const USER_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
@@ -131,19 +113,6 @@ const DEFAULT_AI_CONFIG = {
|
|
|
131
113
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
132
114
|
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
133
115
|
},
|
|
134
|
-
gosuslugi: {
|
|
135
|
-
enabled: false,
|
|
136
|
-
mode: "personal-browser",
|
|
137
|
-
authUrl: "",
|
|
138
|
-
tokenUrl: "",
|
|
139
|
-
userinfoUrl: "",
|
|
140
|
-
clientId: "",
|
|
141
|
-
clientSecret: "",
|
|
142
|
-
scope: "openid",
|
|
143
|
-
redirectHost: "127.0.0.1",
|
|
144
|
-
redirectPort: 18791,
|
|
145
|
-
redirectPath: "/gosuslugi/callback",
|
|
146
|
-
},
|
|
147
116
|
ai: {
|
|
148
117
|
activeProfile: "local",
|
|
149
118
|
provider: "ollama",
|
|
@@ -181,9 +150,6 @@ const DEFAULT_AI_CONFIG = {
|
|
|
181
150
|
export_report: true,
|
|
182
151
|
file_read: false,
|
|
183
152
|
browser_open: true,
|
|
184
|
-
gosuslugi_whoami: true,
|
|
185
|
-
gosuslugi_debt: true,
|
|
186
|
-
gosuslugi_notifications: true,
|
|
187
153
|
files_tree: false,
|
|
188
154
|
files_read: false,
|
|
189
155
|
files_search: false,
|
|
@@ -214,7 +180,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
214
180
|
suggestions: true,
|
|
215
181
|
},
|
|
216
182
|
skills: {
|
|
217
|
-
enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"
|
|
183
|
+
enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
|
|
218
184
|
},
|
|
219
185
|
daemon: {
|
|
220
186
|
host: "127.0.0.1",
|
|
@@ -286,11 +252,6 @@ const SLASH_COMMANDS = [
|
|
|
286
252
|
{ command: "/sessions", description: "AI-сессии" },
|
|
287
253
|
{ command: "/resume SESSION_ID", description: "продолжить сессию" },
|
|
288
254
|
{ command: "/features list", description: "feature flags" },
|
|
289
|
-
{ command: "/gosuslugi status", description: "личное подключение Госуслуг" },
|
|
290
|
-
{ command: "/gosuslugi connect", description: "открыть личный вход Госуслуг" },
|
|
291
|
-
{ command: "/gosuslugi debt", description: "задолженности Госуслуг" },
|
|
292
|
-
{ command: "/gosuslugi notifications", description: "уведомления Госуслуг" },
|
|
293
|
-
{ command: "/gosuslugi keepalive", description: "проверка сессии каждые 30 минут" },
|
|
294
255
|
{ command: "/wiki", description: "ссылки на документацию" },
|
|
295
256
|
{ command: "/context list", description: "локальный контекст проекта" },
|
|
296
257
|
{ command: "/skills list", description: "skills" },
|
|
@@ -361,7 +322,6 @@ const COMMANDS = new Map([
|
|
|
361
322
|
["fork", forkSession],
|
|
362
323
|
["features", handleFeatures],
|
|
363
324
|
["settings", handleSettings],
|
|
364
|
-
["gosuslugi", handleGosuslugi],
|
|
365
325
|
["wiki", handleWiki],
|
|
366
326
|
["context", handleContext],
|
|
367
327
|
["skills", handleSkills],
|
|
@@ -489,7 +449,6 @@ async function showHelp() {
|
|
|
489
449
|
iola agent интерактивный режим
|
|
490
450
|
iola ai setup настройка AI-профиля
|
|
491
451
|
iola browser status браузерный runtime
|
|
492
|
-
iola gosuslugi status личное подключение Госуслуг
|
|
493
452
|
iola mcp status MCP-подключение
|
|
494
453
|
iola doctor диагностика
|
|
495
454
|
iola wiki документация
|
|
@@ -524,7 +483,6 @@ Usage:
|
|
|
524
483
|
iola fork SESSION_ID [TEXT]
|
|
525
484
|
iola features list|enable|disable
|
|
526
485
|
iola settings list|get|validate|doctor|init
|
|
527
|
-
iola gosuslugi terms|consent|status|check|keepalive|install-keepalive|keepalive-status|uninstall-keepalive|connect|open|text|screenshot|whoami|debt|notifications|mark-read|logout|configure|login|userinfo
|
|
528
486
|
iola wiki [open|links]
|
|
529
487
|
iola context list|show|init
|
|
530
488
|
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
@@ -1059,10 +1017,6 @@ async function handleAgentLine(line, state) {
|
|
|
1059
1017
|
return false;
|
|
1060
1018
|
}
|
|
1061
1019
|
|
|
1062
|
-
if (command === "gosuslugi") {
|
|
1063
|
-
await handleGosuslugi(args);
|
|
1064
|
-
return false;
|
|
1065
|
-
}
|
|
1066
1020
|
|
|
1067
1021
|
if (command === "workspace") {
|
|
1068
1022
|
await handleWorkspace(args);
|
|
@@ -1213,7 +1167,6 @@ async function handleAgentLine(line, state) {
|
|
|
1213
1167
|
resume: ["resume", args],
|
|
1214
1168
|
fork: ["fork", args],
|
|
1215
1169
|
features: ["features", args],
|
|
1216
|
-
gosuslugi: ["gosuslugi", args],
|
|
1217
1170
|
wiki: ["wiki", args],
|
|
1218
1171
|
context: ["context", args],
|
|
1219
1172
|
skills: ["skills", args],
|
|
@@ -2245,185 +2198,6 @@ async function handleSettings(args) {
|
|
|
2245
2198
|
throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
|
|
2246
2199
|
}
|
|
2247
2200
|
|
|
2248
|
-
async function handleGosuslugi(args) {
|
|
2249
|
-
const [action = "status", ...rest] = args;
|
|
2250
|
-
const options = parseOptions(rest);
|
|
2251
|
-
|
|
2252
|
-
if (action === "terms") {
|
|
2253
|
-
console.log(GOSUSLUGI_CONSENT_TEXT);
|
|
2254
|
-
return;
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
if (action === "consent") {
|
|
2258
|
-
await acceptGosuslugiConsent(options);
|
|
2259
|
-
return;
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
if (action === "status") {
|
|
2263
|
-
const config = await loadConfig();
|
|
2264
|
-
const secrets = await loadSecrets();
|
|
2265
|
-
const tokens = secrets.gosuslugi?.tokens || null;
|
|
2266
|
-
const browserSession = secrets.gosuslugiBrowser || null;
|
|
2267
|
-
const consent = secrets.gosuslugiConsent || null;
|
|
2268
|
-
printKeyValue({
|
|
2269
|
-
mode: config.gosuslugi?.mode || "personal-browser",
|
|
2270
|
-
enabled: config.gosuslugi?.enabled ? "yes" : "no",
|
|
2271
|
-
browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
2272
|
-
browserProfileExists: existsSync(GOSUSLUGI_BROWSER_PROFILE_DIR) ? "yes" : "no",
|
|
2273
|
-
browserConnected: browserSession?.connectedAt ? "yes" : "unknown",
|
|
2274
|
-
browserConnectedAt: browserSession?.connectedAt || "-",
|
|
2275
|
-
oauthConfigured: isGosuslugiConfigured(config) ? "yes" : "no",
|
|
2276
|
-
consent: consent?.version === GOSUSLUGI_CONSENT_VERSION ? "accepted" : "not accepted",
|
|
2277
|
-
consentAt: consent?.acceptedAt || "-",
|
|
2278
|
-
clientId: config.gosuslugi?.clientId ? maskSecret(config.gosuslugi.clientId) : "-",
|
|
2279
|
-
authUrl: config.gosuslugi?.authUrl || "-",
|
|
2280
|
-
tokenUrl: config.gosuslugi?.tokenUrl || "-",
|
|
2281
|
-
userinfoUrl: config.gosuslugi?.userinfoUrl || "-",
|
|
2282
|
-
redirectUri: gosuslugiRedirectUri(config),
|
|
2283
|
-
connected: tokens?.access_token ? "yes" : "no",
|
|
2284
|
-
savedAt: secrets.gosuslugi?.savedAt || "-",
|
|
2285
|
-
expiresAt: secrets.gosuslugi?.expiresAt || "-",
|
|
2286
|
-
});
|
|
2287
|
-
return;
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
if (action === "check") {
|
|
2291
|
-
const result = await gosuslugiCheck(options);
|
|
2292
|
-
if (options.json) printJson(result);
|
|
2293
|
-
else printKeyValue(result);
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
if (action === "keepalive") {
|
|
2298
|
-
await gosuslugiKeepalive(options);
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
if (action === "install-keepalive") {
|
|
2303
|
-
await installGosuslugiKeepaliveTask(options);
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
if (action === "uninstall-keepalive") {
|
|
2308
|
-
await uninstallGosuslugiKeepaliveTask(options);
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
if (action === "keepalive-status") {
|
|
2313
|
-
await printGosuslugiKeepaliveTaskStatus(options);
|
|
2314
|
-
return;
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
if (action === "connect") {
|
|
2318
|
-
await gosuslugiBrowserConnect(options);
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
if (action === "open") {
|
|
2323
|
-
await gosuslugiBrowserOpen(targetOrDefault(rest, options), options);
|
|
2324
|
-
return;
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
if (action === "text") {
|
|
2328
|
-
const result = await gosuslugiBrowserReadText(targetOrDefault(rest, options), options);
|
|
2329
|
-
if (options.output) {
|
|
2330
|
-
await writeFile(path.resolve(options.output), result, "utf8");
|
|
2331
|
-
console.log(`Файл сохранен: ${path.resolve(options.output)}`);
|
|
2332
|
-
} else {
|
|
2333
|
-
console.log(result);
|
|
2334
|
-
}
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
if (action === "screenshot") {
|
|
2339
|
-
const outputFile = path.resolve(options.output || "gosuslugi-page.png");
|
|
2340
|
-
await gosuslugiBrowserScreenshot(targetOrDefault(rest, options), outputFile, options);
|
|
2341
|
-
saveArtifact("gosuslugi-screenshot", targetOrDefault(rest, options), outputFile, { url: targetOrDefault(rest, options) });
|
|
2342
|
-
console.log(`Файл сохранен: ${outputFile}`);
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
if (action === "whoami" || action === "profile") {
|
|
2347
|
-
const result = await gosuslugiWhoami(options);
|
|
2348
|
-
if (options.json) printJson(result);
|
|
2349
|
-
else printKeyValue(result.summary);
|
|
2350
|
-
return;
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
if (action === "debt" || action === "debts" || action === "payments") {
|
|
2354
|
-
const result = await gosuslugiDebt(options);
|
|
2355
|
-
if (options.json) printJson(result);
|
|
2356
|
-
else printGosuslugiDebt(result);
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
if (action === "notifications" || action === "notices") {
|
|
2361
|
-
const result = await gosuslugiNotifications(options);
|
|
2362
|
-
if (options.json) printJson(result);
|
|
2363
|
-
else printGosuslugiNotifications(result);
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
if (action === "mark-read") {
|
|
2368
|
-
await gosuslugiMarkNotificationsRead(options);
|
|
2369
|
-
return;
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
if (action === "configure") {
|
|
2373
|
-
const current = await loadConfig();
|
|
2374
|
-
const next = {
|
|
2375
|
-
...(current.gosuslugi || {}),
|
|
2376
|
-
enabled: true,
|
|
2377
|
-
mode: "personal-local",
|
|
2378
|
-
authUrl: options["auth-url"] || current.gosuslugi?.authUrl || "",
|
|
2379
|
-
tokenUrl: options["token-url"] || current.gosuslugi?.tokenUrl || "",
|
|
2380
|
-
userinfoUrl: options["userinfo-url"] || current.gosuslugi?.userinfoUrl || "",
|
|
2381
|
-
clientId: options["client-id"] || current.gosuslugi?.clientId || "",
|
|
2382
|
-
clientSecret: options["client-secret"] || current.gosuslugi?.clientSecret || "",
|
|
2383
|
-
scope: options.scope || current.gosuslugi?.scope || "openid",
|
|
2384
|
-
redirectHost: options["redirect-host"] || current.gosuslugi?.redirectHost || "127.0.0.1",
|
|
2385
|
-
redirectPort: Number(options["redirect-port"] || current.gosuslugi?.redirectPort || 18791),
|
|
2386
|
-
redirectPath: options["redirect-path"] || current.gosuslugi?.redirectPath || "/gosuslugi/callback",
|
|
2387
|
-
};
|
|
2388
|
-
await saveConfig({ gosuslugi: next });
|
|
2389
|
-
console.log("Настройки личного локального подключения Госуслуг сохранены.");
|
|
2390
|
-
console.log(`Redirect URI: ${gosuslugiRedirectUri({ gosuslugi: next })}`);
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
if (action === "login") {
|
|
2395
|
-
const result = await gosuslugiLogin(options);
|
|
2396
|
-
printKeyValue(result);
|
|
2397
|
-
return;
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
if (action === "logout") {
|
|
2401
|
-
const secrets = await loadSecrets();
|
|
2402
|
-
delete secrets.gosuslugi;
|
|
2403
|
-
delete secrets.gosuslugiBrowser;
|
|
2404
|
-
await saveSecrets(secrets);
|
|
2405
|
-
if (options.profile || options.all) {
|
|
2406
|
-
await rm(GOSUSLUGI_BROWSER_PROFILE_DIR, { recursive: true, force: true }).catch(() => {});
|
|
2407
|
-
console.log("Локальный браузерный профиль Госуслуг удален.");
|
|
2408
|
-
}
|
|
2409
|
-
console.log("Локальное подключение Госуслуг удалено.");
|
|
2410
|
-
return;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
if (action === "userinfo" || action === "me") {
|
|
2414
|
-
const result = await gosuslugiUserinfo(options);
|
|
2415
|
-
if (options.json) printJson(result);
|
|
2416
|
-
else printKeyValue(flattenObjectForPrint(result));
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
throw new Error("Команды gosuslugi: terms, consent, status, check, keepalive, install-keepalive, keepalive-status, uninstall-keepalive, connect, open, text, screenshot, whoami, debt, notifications, mark-read, logout, configure, login, userinfo.");
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
function targetOrDefault(args, options = {}) {
|
|
2424
|
-
return options.url || args.find((item) => !item.startsWith("--")) || GOSUSLUGI_DEFAULT_URL;
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
2201
|
async function handleWiki(args) {
|
|
2428
2202
|
const [action = "links"] = args;
|
|
2429
2203
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -2431,6 +2205,7 @@ async function handleWiki(args) {
|
|
|
2431
2205
|
["Главная", base],
|
|
2432
2206
|
["Установка", `${base}/Установка`],
|
|
2433
2207
|
["Первый запуск", `${base}/Первый-запуск`],
|
|
2208
|
+
["Мастер настройки", `${base}/Мастер-настройки`],
|
|
2434
2209
|
["AI-профили", `${base}/AI-профили`],
|
|
2435
2210
|
["Локальный инструментальный агент", `${base}/Локальный-инструментальный-агент`],
|
|
2436
2211
|
["Skills и toolsets", `${base}/Skills-и-toolsets`],
|
|
@@ -2438,7 +2213,6 @@ async function handleWiki(args) {
|
|
|
2438
2213
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
2439
2214
|
["Платформа агента", `${base}/Платформа-агента`],
|
|
2440
2215
|
["Браузерный агент", `${base}/Браузерный-агент`],
|
|
2441
|
-
["Подключение Госуслуг", `${base}/Подключение-Госуслуг`],
|
|
2442
2216
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
2443
2217
|
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
2444
2218
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
@@ -3375,189 +3149,6 @@ async function openUrl(url) {
|
|
|
3375
3149
|
await runCommand("xdg-open", [url], { inherit: false });
|
|
3376
3150
|
}
|
|
3377
3151
|
|
|
3378
|
-
async function gosuslugiLogin(options = {}) {
|
|
3379
|
-
const config = await loadConfig();
|
|
3380
|
-
if (!isGosuslugiConfigured(config)) {
|
|
3381
|
-
throw new Error("Личное подключение не настроено. Пример: iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid");
|
|
3382
|
-
}
|
|
3383
|
-
await ensureGosuslugiConsent(options);
|
|
3384
|
-
|
|
3385
|
-
const state = randomUrlSafe(24);
|
|
3386
|
-
const codeVerifier = randomUrlSafe(64);
|
|
3387
|
-
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
3388
|
-
const redirectUri = gosuslugiRedirectUri(config);
|
|
3389
|
-
const callback = waitForOAuthCallback(config.gosuslugi, state, Number(options.timeout || 180000));
|
|
3390
|
-
const authUrl = new URL(config.gosuslugi.authUrl);
|
|
3391
|
-
authUrl.searchParams.set("response_type", "code");
|
|
3392
|
-
authUrl.searchParams.set("client_id", config.gosuslugi.clientId);
|
|
3393
|
-
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
3394
|
-
authUrl.searchParams.set("scope", config.gosuslugi.scope || "openid");
|
|
3395
|
-
authUrl.searchParams.set("state", state);
|
|
3396
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
3397
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
3398
|
-
|
|
3399
|
-
console.log("Открываю экран входа Госуслуг в браузере для личного локального подключения.");
|
|
3400
|
-
console.log("После входа CLI примет callback на локальном адресе и сохранит данные доступа только на этом компьютере.");
|
|
3401
|
-
await openUrl(authUrl.toString());
|
|
3402
|
-
const params = await callback;
|
|
3403
|
-
if (params.error) throw new Error(`Госуслуги вернули ошибку: ${params.error} ${params.error_description || ""}`.trim());
|
|
3404
|
-
if (!params.code) throw new Error("Authorization code не получен.");
|
|
3405
|
-
|
|
3406
|
-
const tokens = await exchangeGosuslugiCode(config, {
|
|
3407
|
-
code: params.code,
|
|
3408
|
-
codeVerifier,
|
|
3409
|
-
redirectUri,
|
|
3410
|
-
});
|
|
3411
|
-
const secrets = await loadSecrets();
|
|
3412
|
-
const now = new Date();
|
|
3413
|
-
const expiresAt = tokens.expires_in ? new Date(now.getTime() + Number(tokens.expires_in) * 1000).toISOString() : "";
|
|
3414
|
-
secrets.gosuslugi = {
|
|
3415
|
-
savedAt: now.toISOString(),
|
|
3416
|
-
expiresAt,
|
|
3417
|
-
tokens,
|
|
3418
|
-
};
|
|
3419
|
-
await saveSecrets(secrets);
|
|
3420
|
-
return {
|
|
3421
|
-
connected: "yes",
|
|
3422
|
-
savedAt: secrets.gosuslugi.savedAt,
|
|
3423
|
-
expiresAt: expiresAt || "-",
|
|
3424
|
-
tokenType: tokens.token_type || "-",
|
|
3425
|
-
scope: tokens.scope || config.gosuslugi.scope || "-",
|
|
3426
|
-
};
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
async function acceptGosuslugiConsent(options = {}) {
|
|
3430
|
-
console.log(GOSUSLUGI_CONSENT_TEXT);
|
|
3431
|
-
if (!options.yes) {
|
|
3432
|
-
const accepted = await confirm("Да, подключить личные Госуслуги к локальному iola-cli? [y/N] ");
|
|
3433
|
-
if (!accepted) {
|
|
3434
|
-
throw new Error("Подключение Госуслуг отменено пользователем.");
|
|
3435
|
-
}
|
|
3436
|
-
}
|
|
3437
|
-
const secrets = await loadSecrets();
|
|
3438
|
-
secrets.gosuslugiConsent = {
|
|
3439
|
-
version: GOSUSLUGI_CONSENT_VERSION,
|
|
3440
|
-
acceptedAt: new Date().toISOString(),
|
|
3441
|
-
user: os.userInfo().username,
|
|
3442
|
-
host: os.hostname(),
|
|
3443
|
-
};
|
|
3444
|
-
await saveSecrets(secrets);
|
|
3445
|
-
console.log("Согласие сохранено локально.");
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
|
-
async function ensureGosuslugiConsent(options = {}) {
|
|
3449
|
-
const secrets = await loadSecrets();
|
|
3450
|
-
if (secrets.gosuslugiConsent?.version === GOSUSLUGI_CONSENT_VERSION) return;
|
|
3451
|
-
await acceptGosuslugiConsent(options);
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
async function requireGosuslugiConsent() {
|
|
3455
|
-
await ensureGosuslugiConsent();
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
function waitForOAuthCallback(settings, expectedState, timeoutMs) {
|
|
3459
|
-
const host = settings.redirectHost || "127.0.0.1";
|
|
3460
|
-
const port = Number(settings.redirectPort || 18791);
|
|
3461
|
-
const callbackPath = settings.redirectPath || "/gosuslugi/callback";
|
|
3462
|
-
return new Promise((resolve, reject) => {
|
|
3463
|
-
const timer = setTimeout(() => {
|
|
3464
|
-
server.close(() => {});
|
|
3465
|
-
reject(new Error("Истекло время ожидания входа через Госуслуги."));
|
|
3466
|
-
}, timeoutMs);
|
|
3467
|
-
const server = createServer((req, res) => {
|
|
3468
|
-
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
3469
|
-
if (url.pathname !== callbackPath) {
|
|
3470
|
-
res.statusCode = 404;
|
|
3471
|
-
res.end("Not found");
|
|
3472
|
-
return;
|
|
3473
|
-
}
|
|
3474
|
-
const params = Object.fromEntries(url.searchParams.entries());
|
|
3475
|
-
if (params.state !== expectedState) {
|
|
3476
|
-
res.statusCode = 400;
|
|
3477
|
-
res.end("Invalid state");
|
|
3478
|
-
clearTimeout(timer);
|
|
3479
|
-
server.close(() => {});
|
|
3480
|
-
reject(new Error("OAuth state не совпал. Вход отменен."));
|
|
3481
|
-
return;
|
|
3482
|
-
}
|
|
3483
|
-
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
3484
|
-
res.end("<!doctype html><meta charset=\"utf-8\"><title>iola</title><body>Вход выполнен. Можно закрыть это окно и вернуться в терминал.</body>");
|
|
3485
|
-
clearTimeout(timer);
|
|
3486
|
-
server.close(() => resolve(params));
|
|
3487
|
-
});
|
|
3488
|
-
server.once("error", (error) => {
|
|
3489
|
-
clearTimeout(timer);
|
|
3490
|
-
reject(error);
|
|
3491
|
-
});
|
|
3492
|
-
server.listen(port, host);
|
|
3493
|
-
});
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
async function exchangeGosuslugiCode(config, { code, codeVerifier, redirectUri }) {
|
|
3497
|
-
const body = new URLSearchParams();
|
|
3498
|
-
body.set("grant_type", "authorization_code");
|
|
3499
|
-
body.set("code", code);
|
|
3500
|
-
body.set("redirect_uri", redirectUri);
|
|
3501
|
-
body.set("client_id", config.gosuslugi.clientId);
|
|
3502
|
-
body.set("code_verifier", codeVerifier);
|
|
3503
|
-
body.set("client_mode", config.gosuslugi.mode || "personal-local");
|
|
3504
|
-
if (config.gosuslugi.clientSecret) body.set("client_secret", config.gosuslugi.clientSecret);
|
|
3505
|
-
|
|
3506
|
-
const response = await fetch(config.gosuslugi.tokenUrl, {
|
|
3507
|
-
method: "POST",
|
|
3508
|
-
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
3509
|
-
body,
|
|
3510
|
-
});
|
|
3511
|
-
const text = await response.text();
|
|
3512
|
-
let payload = {};
|
|
3513
|
-
try {
|
|
3514
|
-
payload = text ? JSON.parse(text) : {};
|
|
3515
|
-
} catch {
|
|
3516
|
-
payload = { raw: text };
|
|
3517
|
-
}
|
|
3518
|
-
if (!response.ok) {
|
|
3519
|
-
throw new Error(`Token endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
3520
|
-
}
|
|
3521
|
-
return payload;
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
async function gosuslugiUserinfo() {
|
|
3525
|
-
const config = await loadConfig();
|
|
3526
|
-
const secrets = await loadSecrets();
|
|
3527
|
-
const accessToken = secrets.gosuslugi?.tokens?.access_token;
|
|
3528
|
-
if (!accessToken) throw new Error("Госуслуги не подключены. Запустите: iola gosuslugi login");
|
|
3529
|
-
if (!config.gosuslugi?.userinfoUrl) throw new Error("userinfoUrl не настроен.");
|
|
3530
|
-
const response = await fetch(config.gosuslugi.userinfoUrl, {
|
|
3531
|
-
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
|
3532
|
-
});
|
|
3533
|
-
const text = await response.text();
|
|
3534
|
-
let payload = {};
|
|
3535
|
-
try {
|
|
3536
|
-
payload = text ? JSON.parse(text) : {};
|
|
3537
|
-
} catch {
|
|
3538
|
-
payload = { raw: text };
|
|
3539
|
-
}
|
|
3540
|
-
if (!response.ok) throw new Error(`Userinfo endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
3541
|
-
return payload;
|
|
3542
|
-
}
|
|
3543
|
-
|
|
3544
|
-
function isGosuslugiConfigured(config) {
|
|
3545
|
-
return Boolean(config.gosuslugi?.authUrl && config.gosuslugi?.tokenUrl && config.gosuslugi?.clientId);
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
|
-
function gosuslugiRedirectUri(config) {
|
|
3549
|
-
const settings = config.gosuslugi || DEFAULT_AI_CONFIG.gosuslugi;
|
|
3550
|
-
return `http://${settings.redirectHost || "127.0.0.1"}:${Number(settings.redirectPort || 18791)}${settings.redirectPath || "/gosuslugi/callback"}`;
|
|
3551
|
-
}
|
|
3552
|
-
|
|
3553
|
-
function randomUrlSafe(bytes) {
|
|
3554
|
-
return base64Url(randomBytes(bytes));
|
|
3555
|
-
}
|
|
3556
|
-
|
|
3557
|
-
function base64Url(buffer) {
|
|
3558
|
-
return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
3152
|
function maskSecret(value) {
|
|
3562
3153
|
const text = String(value || "");
|
|
3563
3154
|
if (text.length <= 8) return text ? "***" : "-";
|
|
@@ -6074,12 +5665,6 @@ async function aiAsk(args, context = {}) {
|
|
|
6074
5665
|
throw new Error('Текст вопроса обязателен. Пример: iola ai ask "Какие школы есть на улице Петрова?"');
|
|
6075
5666
|
}
|
|
6076
5667
|
|
|
6077
|
-
if (!options.bare && isGosuslugiPersonalIntent(question)) {
|
|
6078
|
-
const answer = await answerGosuslugiQuestion(question, options);
|
|
6079
|
-
if (!options.quiet) console.log(answer);
|
|
6080
|
-
return answer;
|
|
6081
|
-
}
|
|
6082
|
-
|
|
6083
5668
|
const config = await loadConfig();
|
|
6084
5669
|
const providerConfig = await resolveUsableAiProfile(config, options);
|
|
6085
5670
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
@@ -6248,7 +5833,7 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
6248
5833
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
6249
5834
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
6250
5835
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
6251
|
-
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}
|
|
5836
|
+
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
6252
5837
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
6253
5838
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
6254
5839
|
`Вопрос: ${question}`,
|
|
@@ -6278,12 +5863,6 @@ function inferToolPlan(question, options = {}) {
|
|
|
6278
5863
|
const steps = [];
|
|
6279
5864
|
if (normalized.includes("без телефона")) {
|
|
6280
5865
|
steps.push({ tool: "export_report", args: { name: "missing-phones" } });
|
|
6281
|
-
} else if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
|
|
6282
|
-
steps.push({ tool: "gosuslugi_notifications", args: { unread: /непрочитан|нов/iu.test(normalized), limit: 15 } });
|
|
6283
|
-
} else if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
|
|
6284
|
-
steps.push({ tool: "gosuslugi_debt", args: {} });
|
|
6285
|
-
} else if (/(фио|дата рождения|профиль|кто я)/iu.test(normalized) && /госуслуг/iu.test(normalized)) {
|
|
6286
|
-
steps.push({ tool: "gosuslugi_whoami", args: {} });
|
|
6287
5866
|
} else {
|
|
6288
5867
|
const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
|
|
6289
5868
|
steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
|
|
@@ -6373,18 +5952,6 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6373
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" });
|
|
6374
5953
|
current = [{ url: step.args?.url, text }];
|
|
6375
5954
|
outputs.push({ tool: step.tool, rows: 1 });
|
|
6376
|
-
} else if (step.tool === "gosuslugi_whoami") {
|
|
6377
|
-
const result = await gosuslugiWhoami(step.args || {});
|
|
6378
|
-
current = [result.summary];
|
|
6379
|
-
outputs.push({ tool: step.tool, rows: 1 });
|
|
6380
|
-
} else if (step.tool === "gosuslugi_debt") {
|
|
6381
|
-
const result = await gosuslugiDebt(step.args || {});
|
|
6382
|
-
current = [{ total: result.total, amount: result.amount, debts: result.debts }];
|
|
6383
|
-
outputs.push({ tool: step.tool, rows: result.debts.length });
|
|
6384
|
-
} else if (step.tool === "gosuslugi_notifications") {
|
|
6385
|
-
const result = await gosuslugiNotifications(step.args || {});
|
|
6386
|
-
current = [{ total: result.total, unread: result.unread, items: result.items }];
|
|
6387
|
-
outputs.push({ tool: step.tool, rows: result.items.length });
|
|
6388
5955
|
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
6389
5956
|
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
6390
5957
|
current = Array.isArray(result) ? result : [result];
|
|
@@ -7094,7 +6661,8 @@ async function onboard(args = []) {
|
|
|
7094
6661
|
}
|
|
7095
6662
|
}
|
|
7096
6663
|
|
|
7097
|
-
const
|
|
6664
|
+
const componentStatus = await getOnboardComponentStatus();
|
|
6665
|
+
const components = options.yes ? defaultOnboardComponents(componentStatus) : await chooseOnboardComponents(componentStatus);
|
|
7098
6666
|
if (components.includes("workspace")) await handleWorkspace(["init"]);
|
|
7099
6667
|
if (components.includes("policy")) await handlePolicy(["use", "analyst"]);
|
|
7100
6668
|
if (components.includes("archive")) await ensureArchiveTool({ install: true });
|
|
@@ -7120,17 +6688,6 @@ async function onboard(args = []) {
|
|
|
7120
6688
|
if (status.installed === "yes") console.log("Browser runtime уже установлен.");
|
|
7121
6689
|
else await installBrowserRuntime();
|
|
7122
6690
|
}
|
|
7123
|
-
if (components.includes("gosuslugi")) {
|
|
7124
|
-
if (process.stdin.isTTY) await handleGosuslugi(["consent"]);
|
|
7125
|
-
else await handleGosuslugi(["terms"]);
|
|
7126
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7127
|
-
if (process.stdin.isTTY && await confirm("Открыть Госуслуги для входа сейчас? [Y/n] ")) {
|
|
7128
|
-
await gosuslugiBrowserConnect({ yes: true });
|
|
7129
|
-
await installGosuslugiKeepaliveTask({ interval: "30m" });
|
|
7130
|
-
} else {
|
|
7131
|
-
console.log("Подключить личные Госуслуги позже: iola gosuslugi connect");
|
|
7132
|
-
}
|
|
7133
|
-
}
|
|
7134
6691
|
if (components.includes("index")) {
|
|
7135
6692
|
await setFilesMode("read-only", await loadConfig());
|
|
7136
6693
|
console.log("Индекс документов можно запустить командой: iola index folder ./docs");
|
|
@@ -7139,25 +6696,19 @@ async function onboard(args = []) {
|
|
|
7139
6696
|
console.log("Onboard завершен.");
|
|
7140
6697
|
}
|
|
7141
6698
|
|
|
7142
|
-
async function chooseOnboardComponents() {
|
|
6699
|
+
async function chooseOnboardComponents(status = null) {
|
|
7143
6700
|
if (!process.stdin.isTTY) return ["workspace", "policy"];
|
|
6701
|
+
const componentStatus = status || await getOnboardComponentStatus();
|
|
7144
6702
|
console.log("");
|
|
7145
6703
|
console.log("Выберите компоненты через запятую:");
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
console.log("4. OpenAI API");
|
|
7150
|
-
console.log("5. OpenRouter API");
|
|
7151
|
-
console.log("6. Codex CLI");
|
|
7152
|
-
console.log("7. MCP для Codex");
|
|
7153
|
-
console.log("8. 7-Zip / архивы");
|
|
7154
|
-
console.log("9. Индекс локальных документов");
|
|
7155
|
-
console.log("10. Browser runtime");
|
|
7156
|
-
console.log("11. Личное подключение Госуслуг");
|
|
6704
|
+
for (const item of onboardComponentRows(componentStatus)) {
|
|
6705
|
+
console.log(`${item.number}. ${item.title} [${item.status}] - ${item.hint}`);
|
|
6706
|
+
}
|
|
7157
6707
|
console.log("");
|
|
7158
6708
|
const rl = readline.createInterface({ input, output });
|
|
7159
6709
|
try {
|
|
7160
|
-
const
|
|
6710
|
+
const defaults = defaultOnboardSelection(componentStatus);
|
|
6711
|
+
const answer = (await rl.question(`Компоненты [${defaults.join(",")}]: `)).trim() || defaults.join(",");
|
|
7161
6712
|
const selected = new Set(answer.split(/[,\s]+/).filter(Boolean));
|
|
7162
6713
|
const map = {
|
|
7163
6714
|
1: "workspace",
|
|
@@ -7170,7 +6721,6 @@ async function chooseOnboardComponents() {
|
|
|
7170
6721
|
8: "archive",
|
|
7171
6722
|
9: "index",
|
|
7172
6723
|
10: "browser",
|
|
7173
|
-
11: "gosuslugi",
|
|
7174
6724
|
};
|
|
7175
6725
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
7176
6726
|
} finally {
|
|
@@ -7178,6 +6728,60 @@ async function chooseOnboardComponents() {
|
|
|
7178
6728
|
}
|
|
7179
6729
|
}
|
|
7180
6730
|
|
|
6731
|
+
async function getOnboardComponentStatus() {
|
|
6732
|
+
const [config, readiness, browser, archive, codexVersion, ollamaVersion] = await Promise.all([
|
|
6733
|
+
loadConfig(),
|
|
6734
|
+
getAiReadiness(),
|
|
6735
|
+
getBrowserStatus(),
|
|
6736
|
+
findCommand(["7z", "7zz", "7za"], ["--help"]).catch(() => null),
|
|
6737
|
+
getCommandVersion("codex", ["--version"]),
|
|
6738
|
+
getOllamaVersion(),
|
|
6739
|
+
]);
|
|
6740
|
+
const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
|
|
6741
|
+
const policyReady = (config.toolsets?.enabled || []).includes("analyst");
|
|
6742
|
+
return {
|
|
6743
|
+
workspace: workspaceReady,
|
|
6744
|
+
policy: policyReady,
|
|
6745
|
+
ollama: Boolean(ollamaVersion && readiness.ollama),
|
|
6746
|
+
openai: Boolean(readiness.openai),
|
|
6747
|
+
openrouter: Boolean(readiness.openrouter),
|
|
6748
|
+
codex: Boolean(codexVersion !== "не найден" && readiness.codex),
|
|
6749
|
+
"codex-mcp": false,
|
|
6750
|
+
archive: Boolean(archive),
|
|
6751
|
+
index: false,
|
|
6752
|
+
browser: browser.installed === "yes",
|
|
6753
|
+
};
|
|
6754
|
+
}
|
|
6755
|
+
|
|
6756
|
+
function onboardComponentRows(status) {
|
|
6757
|
+
const rows = [
|
|
6758
|
+
["1", "workspace", "workspace и контекст", "рабочая папка, IOLA.md и .iola/context.md"],
|
|
6759
|
+
["2", "policy", "policy analyst", "разрешения и профиль аналитика"],
|
|
6760
|
+
["3", "ollama", "Ollama + локальная модель", "локальная модель найдена"],
|
|
6761
|
+
["4", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
|
|
6762
|
+
["5", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
|
|
6763
|
+
["6", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
|
|
6764
|
+
["7", "codex-mcp", "MCP для Codex", "можно переустановить/обновить"],
|
|
6765
|
+
["8", "archive", "7-Zip / архивы", "архиватор найден"],
|
|
6766
|
+
["9", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
|
|
6767
|
+
["10", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
6768
|
+
];
|
|
6769
|
+
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
6770
|
+
}
|
|
6771
|
+
|
|
6772
|
+
function defaultOnboardSelection(status) {
|
|
6773
|
+
const defaults = [];
|
|
6774
|
+
if (!status.workspace) defaults.push("1");
|
|
6775
|
+
if (!status.policy) defaults.push("2");
|
|
6776
|
+
if (!status.archive) defaults.push("8");
|
|
6777
|
+
return defaults.length ? defaults : ["1", "2"];
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6780
|
+
function defaultOnboardComponents(status) {
|
|
6781
|
+
const map = { 1: "workspace", 2: "policy", 3: "ollama", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser" };
|
|
6782
|
+
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
6783
|
+
}
|
|
6784
|
+
|
|
7181
6785
|
function parseOptions(args) {
|
|
7182
6786
|
const result = { _: [] };
|
|
7183
6787
|
|
|
@@ -7424,8 +7028,7 @@ async function buildSkillsText(config, question = "", options = {}) {
|
|
|
7424
7028
|
const chunks = [];
|
|
7425
7029
|
const selected = selectSkillsForPrompt(config, question, options);
|
|
7426
7030
|
for (const skill of listSkills(config)) {
|
|
7427
|
-
|
|
7428
|
-
if (!active || !selected.has(skill.name)) continue;
|
|
7031
|
+
if (!skill.enabled || !selected.has(skill.name)) continue;
|
|
7429
7032
|
const text = await readFile(skill.file, "utf8");
|
|
7430
7033
|
chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
|
|
7431
7034
|
}
|
|
@@ -7441,7 +7044,6 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
7441
7044
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
7442
7045
|
if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
7443
7046
|
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
7444
|
-
if (enabled.has("gosuslugi") && /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized)) selected.add("gosuslugi");
|
|
7445
7047
|
return selected;
|
|
7446
7048
|
}
|
|
7447
7049
|
|
|
@@ -7665,534 +7267,6 @@ async function runBrowserAutomation(action, params) {
|
|
|
7665
7267
|
}
|
|
7666
7268
|
}
|
|
7667
7269
|
|
|
7668
|
-
async function ensureBrowserRuntimeForGosuslugi() {
|
|
7669
|
-
if (existsSync(BROWSER_RUNTIME_PACKAGE)) return;
|
|
7670
|
-
console.log("Browser runtime не установлен. Устанавливаю Playwright/Chromium для локального браузерного профиля.");
|
|
7671
|
-
await installBrowserRuntime();
|
|
7672
|
-
}
|
|
7673
|
-
|
|
7674
|
-
async function gosuslugiBrowserConnect(options = {}) {
|
|
7675
|
-
await ensureGosuslugiConsent({ yes: options.yes });
|
|
7676
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7677
|
-
await saveConfig({ gosuslugi: { ...(await loadConfig()).gosuslugi, enabled: true, mode: "personal-browser" } });
|
|
7678
|
-
const url = options.url || GOSUSLUGI_DEFAULT_URL;
|
|
7679
|
-
console.log(`Открываю Госуслуги в отдельном локальном профиле: ${GOSUSLUGI_BROWSER_PROFILE_DIR}`);
|
|
7680
|
-
console.log("Авторизуйтесь в открывшемся окне. Когда закончите, закройте окно браузера.");
|
|
7681
|
-
await runPersistentBrowserAutomation("open", {
|
|
7682
|
-
url,
|
|
7683
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7684
|
-
headed: true,
|
|
7685
|
-
waitMs: Number(options.wait || 0),
|
|
7686
|
-
timeout: Number(options.timeout || 120000),
|
|
7687
|
-
viewport: options.viewport || "1366x768",
|
|
7688
|
-
});
|
|
7689
|
-
const secrets = await loadSecrets();
|
|
7690
|
-
secrets.gosuslugiBrowser = {
|
|
7691
|
-
mode: "personal-browser",
|
|
7692
|
-
profileDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7693
|
-
connectedAt: new Date().toISOString(),
|
|
7694
|
-
lastUrl: url,
|
|
7695
|
-
};
|
|
7696
|
-
await saveSecrets(secrets);
|
|
7697
|
-
console.log("Локальный браузерный профиль Госуслуг сохранен.");
|
|
7698
|
-
}
|
|
7699
|
-
|
|
7700
|
-
async function gosuslugiBrowserOpen(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
|
|
7701
|
-
await requireGosuslugiConsent();
|
|
7702
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7703
|
-
await runPersistentBrowserAutomation("open", {
|
|
7704
|
-
url,
|
|
7705
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7706
|
-
headed: true,
|
|
7707
|
-
waitMs: Number(options.wait || 0),
|
|
7708
|
-
timeout: Number(options.timeout || 120000),
|
|
7709
|
-
viewport: options.viewport || "1366x768",
|
|
7710
|
-
});
|
|
7711
|
-
}
|
|
7712
|
-
|
|
7713
|
-
async function gosuslugiBrowserReadText(url = GOSUSLUGI_DEFAULT_URL, options = {}) {
|
|
7714
|
-
await requireGosuslugiConsent();
|
|
7715
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7716
|
-
return runPersistentBrowserAutomation("text", {
|
|
7717
|
-
url,
|
|
7718
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7719
|
-
headed: Boolean(options.headed),
|
|
7720
|
-
waitMs: Number(options.wait || 3000),
|
|
7721
|
-
timeout: Number(options.timeout || 60000),
|
|
7722
|
-
viewport: options.viewport || "1366x768",
|
|
7723
|
-
});
|
|
7724
|
-
}
|
|
7725
|
-
|
|
7726
|
-
async function gosuslugiBrowserScreenshot(url = GOSUSLUGI_DEFAULT_URL, outputFile, options = {}) {
|
|
7727
|
-
await requireGosuslugiConsent();
|
|
7728
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
7729
|
-
await runPersistentBrowserAutomation("screenshot", {
|
|
7730
|
-
url,
|
|
7731
|
-
output: outputFile,
|
|
7732
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
7733
|
-
headed: Boolean(options.headed),
|
|
7734
|
-
waitMs: Number(options.wait || 3000),
|
|
7735
|
-
timeout: Number(options.timeout || 60000),
|
|
7736
|
-
viewport: options.viewport || "1366x768",
|
|
7737
|
-
});
|
|
7738
|
-
}
|
|
7739
|
-
|
|
7740
|
-
async function gosuslugiWhoami(options = {}) {
|
|
7741
|
-
const data = await gosuslugiBrowserApiJson({
|
|
7742
|
-
pageUrl: "https://lk.gosuslugi.ru/settings/account",
|
|
7743
|
-
endpoint: "https://www.gosuslugi.ru/api/lk/v1/users/data",
|
|
7744
|
-
waitMs: Number(options.wait || 3000),
|
|
7745
|
-
});
|
|
7746
|
-
const person = data.person?.person || data.person || data;
|
|
7747
|
-
const summary = {
|
|
7748
|
-
fio: [data.lastName || person.lastName, data.firstName || person.firstName, data.middleName || person.middleName].filter(Boolean).join(" ") || data.formattedName || "-",
|
|
7749
|
-
birthDate: person.birthDate || data.birthDate || "-",
|
|
7750
|
-
status: data.assuranceLevel === "AL20" || person.trusted ? "Подтвержденная учетная запись" : data.assuranceLevel || "-",
|
|
7751
|
-
phone: options.full ? (data.personMobilePhone || data.mobile || "-") : maskPhone(data.personMobilePhone || data.mobile || ""),
|
|
7752
|
-
email: options.full ? (data.personEMail || data.personEmail || data.email || "-") : maskEmail(data.personEMail || data.personEmail || data.email || ""),
|
|
7753
|
-
snils: options.full ? (person.snils || data.personSnils || data.snils || "-") : maskDocument(person.snils || data.personSnils || data.snils || ""),
|
|
7754
|
-
inn: options.full ? (person.inn || data.personINN || data.inn || "-") : maskDocument(person.inn || data.personINN || data.inn || ""),
|
|
7755
|
-
};
|
|
7756
|
-
return {
|
|
7757
|
-
summary,
|
|
7758
|
-
raw: options.full ? redactGosuslugiSensitive(data, { keepPersonal: true }) : undefined,
|
|
7759
|
-
};
|
|
7760
|
-
}
|
|
7761
|
-
|
|
7762
|
-
async function gosuslugiDebt(options = {}) {
|
|
7763
|
-
const data = await gosuslugiBrowserApiJson({
|
|
7764
|
-
pageUrl: "https://www.gosuslugi.ru/pay/forPayment",
|
|
7765
|
-
endpoint: "https://www.gosuslugi.ru/api/pay/v2/informer/fetch",
|
|
7766
|
-
waitMs: Number(options.wait || 5000),
|
|
7767
|
-
});
|
|
7768
|
-
const groups = Array.isArray(data.groups) ? data.groups : [];
|
|
7769
|
-
const debts = groups.flatMap((group) => (group.bills || []).map((bill) => ({
|
|
7770
|
-
group: group.name || group.code || "-",
|
|
7771
|
-
caption: bill.caption || "-",
|
|
7772
|
-
amount: Number(bill.amount || 0),
|
|
7773
|
-
billDate: bill.billDate || "-",
|
|
7774
|
-
supplier: bill.supplierFullName || "-",
|
|
7775
|
-
document: bill.document?.typeName ? `${bill.document.typeName} ${bill.document.number || ""}`.trim() : "-",
|
|
7776
|
-
})));
|
|
7777
|
-
return {
|
|
7778
|
-
total: Number(data.summary?.total || debts.length || 0),
|
|
7779
|
-
amount: Number(data.summary?.amount || debts.reduce((sum, item) => sum + item.amount, 0)),
|
|
7780
|
-
groups: groups.map((group) => ({ name: group.name, code: group.code, total: group.summary?.total || 0, amount: group.summary?.amount || 0 })),
|
|
7781
|
-
debts,
|
|
7782
|
-
};
|
|
7783
|
-
}
|
|
7784
|
-
|
|
7785
|
-
async function gosuslugiNotifications(options = {}) {
|
|
7786
|
-
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";
|
|
7787
|
-
const pageSize = Number(options.limit || 15);
|
|
7788
|
-
const unread = options.unread ? "true" : "false";
|
|
7789
|
-
const counters = await gosuslugiBrowserApiJson({
|
|
7790
|
-
pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
|
|
7791
|
-
endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/counters?types=${types},PARTNERS&isArchive=false`,
|
|
7792
|
-
waitMs: Number(options.wait || 3000),
|
|
7793
|
-
});
|
|
7794
|
-
const feed = await gosuslugiBrowserApiJson({
|
|
7795
|
-
pageUrl: `https://lk.gosuslugi.ru/notifications?type=${types}`,
|
|
7796
|
-
endpoint: `https://www.gosuslugi.ru/api/lk/v1/feeds/?unread=${unread}&isArchive=false&isHide=false&types=${types}&pageSize=${pageSize}&status=&startDate=&lastFeedId=&lastFeedDate=&q=`,
|
|
7797
|
-
waitMs: Number(options.wait || 3000),
|
|
7798
|
-
});
|
|
7799
|
-
const items = (feed.items || []).map((item) => ({
|
|
7800
|
-
id: item.id,
|
|
7801
|
-
unread: Boolean(item.unread),
|
|
7802
|
-
date: item.date || "-",
|
|
7803
|
-
type: item.feedType || "-",
|
|
7804
|
-
title: item.title || "-",
|
|
7805
|
-
subtitle: item.subTitle || "-",
|
|
7806
|
-
status: item.status || "-",
|
|
7807
|
-
summary: summarizeNotificationData(item.data),
|
|
7808
|
-
}));
|
|
7809
|
-
return {
|
|
7810
|
-
total: counters.total || feed.items?.length || 0,
|
|
7811
|
-
unread: counters.unread || items.filter((item) => item.unread).length,
|
|
7812
|
-
counters: counters.counter || [],
|
|
7813
|
-
hasMore: Boolean(feed.hasMore),
|
|
7814
|
-
items,
|
|
7815
|
-
};
|
|
7816
|
-
}
|
|
7817
|
-
|
|
7818
|
-
async function gosuslugiMarkNotificationsRead(options = {}) {
|
|
7819
|
-
await requireGosuslugiConsent();
|
|
7820
|
-
if (!options.yes) {
|
|
7821
|
-
const ok = await confirm("Отметить уведомления Госуслуг прочитанными? Это изменит состояние личного кабинета. [y/N] ");
|
|
7822
|
-
if (!ok) {
|
|
7823
|
-
console.log("Операция отменена.");
|
|
7824
|
-
return;
|
|
7825
|
-
}
|
|
7826
|
-
}
|
|
7827
|
-
await gosuslugiBrowserClickText({
|
|
7828
|
-
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",
|
|
7829
|
-
text: "Прочитать все",
|
|
7830
|
-
waitMs: Number(options.wait || 5000),
|
|
7831
|
-
});
|
|
7832
|
-
console.log("Команда отметки прочитанным выполнена. Проверьте статус: iola gosuslugi notifications --unread");
|
|
7833
|
-
}
|
|
7834
|
-
|
|
7835
|
-
async function gosuslugiCheck(options = {}) {
|
|
7836
|
-
try {
|
|
7837
|
-
const result = await gosuslugiWhoami({ wait: options.wait || 2000 });
|
|
7838
|
-
return {
|
|
7839
|
-
status: "ok",
|
|
7840
|
-
authorized: "yes",
|
|
7841
|
-
fio: result.summary.fio,
|
|
7842
|
-
checkedAt: new Date().toISOString(),
|
|
7843
|
-
nextAction: "-",
|
|
7844
|
-
};
|
|
7845
|
-
} catch (error) {
|
|
7846
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7847
|
-
const result = {
|
|
7848
|
-
status: "needs-login",
|
|
7849
|
-
authorized: "unknown",
|
|
7850
|
-
checkedAt: new Date().toISOString(),
|
|
7851
|
-
nextAction: "iola gosuslugi connect",
|
|
7852
|
-
error: message,
|
|
7853
|
-
};
|
|
7854
|
-
if (!options.silent) {
|
|
7855
|
-
console.error("Сессия Госуслуг недоступна или требует повторный вход.");
|
|
7856
|
-
console.error("Запустите: iola gosuslugi connect");
|
|
7857
|
-
}
|
|
7858
|
-
return result;
|
|
7859
|
-
}
|
|
7860
|
-
}
|
|
7861
|
-
|
|
7862
|
-
async function gosuslugiKeepalive(options = {}) {
|
|
7863
|
-
const intervalMs = parseDurationMs(options.interval || "30m");
|
|
7864
|
-
const once = Boolean(options.once);
|
|
7865
|
-
console.log(`Gosuslugi keepalive запущен. Интервал: ${Math.round(intervalMs / 60000)} мин.`);
|
|
7866
|
-
console.log("Остановить: Ctrl+C");
|
|
7867
|
-
while (true) {
|
|
7868
|
-
const result = await gosuslugiCheck({ silent: true });
|
|
7869
|
-
const line = result.status === "ok"
|
|
7870
|
-
? `[${result.checkedAt}] Госуслуги: сессия активна (${result.fio || "-"})`
|
|
7871
|
-
: `[${result.checkedAt}] Госуслуги: нужен повторный вход. Запустите: iola gosuslugi connect`;
|
|
7872
|
-
console.log(line);
|
|
7873
|
-
if (once) return;
|
|
7874
|
-
await sleep(intervalMs);
|
|
7875
|
-
}
|
|
7876
|
-
}
|
|
7877
|
-
|
|
7878
|
-
function gosuslugiKeepaliveTaskName() {
|
|
7879
|
-
return "iola-gosuslugi-keepalive";
|
|
7880
|
-
}
|
|
7881
|
-
|
|
7882
|
-
function gosuslugiKeepaliveLogFile() {
|
|
7883
|
-
return path.join(CONFIG_DIR, "gosuslugi-keepalive.log");
|
|
7884
|
-
}
|
|
7885
|
-
|
|
7886
|
-
function cliEntrypointFile() {
|
|
7887
|
-
return path.resolve(__dirname, "..", "bin", "iola.js");
|
|
7888
|
-
}
|
|
7889
|
-
|
|
7890
|
-
async function installGosuslugiKeepaliveTask(options = {}) {
|
|
7891
|
-
const intervalMinutes = Math.max(1, Math.round(parseDurationMs(options.interval || "30m") / 60000));
|
|
7892
|
-
if (process.platform === "win32") {
|
|
7893
|
-
await installWindowsGosuslugiKeepaliveTask(intervalMinutes);
|
|
7894
|
-
return;
|
|
7895
|
-
}
|
|
7896
|
-
const id = addCronJob(`каждые ${intervalMinutes} минут`, "gosuslugi check --silent");
|
|
7897
|
-
console.log(`Локальная cron-задача добавлена: ${id}`);
|
|
7898
|
-
console.log("Для автоматического выполнения настройте системный планировщик на запуск: iola cron tick");
|
|
7899
|
-
}
|
|
7900
|
-
|
|
7901
|
-
async function installWindowsGosuslugiKeepaliveTask(intervalMinutes) {
|
|
7902
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
7903
|
-
const taskName = gosuslugiKeepaliveTaskName();
|
|
7904
|
-
const logFile = gosuslugiKeepaliveLogFile();
|
|
7905
|
-
const script = path.join(CONFIG_DIR, "gosuslugi-keepalive-task.cmd");
|
|
7906
|
-
const command = `"${process.execPath}" --no-warnings "${cliEntrypointFile()}" gosuslugi check --silent >> "${logFile}" 2>&1`;
|
|
7907
|
-
await writeFile(script, `@echo off\r\n${command}\r\n`, "utf8");
|
|
7908
|
-
await runCommand("schtasks.exe", [
|
|
7909
|
-
"/Create",
|
|
7910
|
-
"/TN", taskName,
|
|
7911
|
-
"/SC", "MINUTE",
|
|
7912
|
-
"/MO", String(intervalMinutes),
|
|
7913
|
-
"/TR", script,
|
|
7914
|
-
"/F",
|
|
7915
|
-
]);
|
|
7916
|
-
console.log(`Windows Task Scheduler задача создана: ${taskName}`);
|
|
7917
|
-
console.log(`Интервал: ${intervalMinutes} мин.`);
|
|
7918
|
-
console.log(`Лог: ${logFile}`);
|
|
7919
|
-
console.log("Проверить: iola gosuslugi keepalive-status");
|
|
7920
|
-
}
|
|
7921
|
-
|
|
7922
|
-
async function uninstallGosuslugiKeepaliveTask() {
|
|
7923
|
-
if (process.platform === "win32") {
|
|
7924
|
-
await runCommand("schtasks.exe", ["/Delete", "/TN", gosuslugiKeepaliveTaskName(), "/F"]).catch(() => {});
|
|
7925
|
-
console.log(`Windows Task Scheduler задача удалена: ${gosuslugiKeepaliveTaskName()}`);
|
|
7926
|
-
return;
|
|
7927
|
-
}
|
|
7928
|
-
console.log("Для не-Windows удалите локальную cron-задачу вручную: iola cron list, затем iola cron delete ID.");
|
|
7929
|
-
}
|
|
7930
|
-
|
|
7931
|
-
async function printGosuslugiKeepaliveTaskStatus(options = {}) {
|
|
7932
|
-
if (process.platform === "win32") {
|
|
7933
|
-
try {
|
|
7934
|
-
const { stdout } = await runCommand("schtasks.exe", ["/Query", "/TN", gosuslugiKeepaliveTaskName(), "/FO", "LIST"]);
|
|
7935
|
-
console.log(stdout.trim());
|
|
7936
|
-
} catch {
|
|
7937
|
-
console.log(`Задача не найдена: ${gosuslugiKeepaliveTaskName()}`);
|
|
7938
|
-
}
|
|
7939
|
-
if (existsSync(gosuslugiKeepaliveLogFile())) {
|
|
7940
|
-
console.log("");
|
|
7941
|
-
console.log(`Лог: ${gosuslugiKeepaliveLogFile()}`);
|
|
7942
|
-
}
|
|
7943
|
-
return;
|
|
7944
|
-
}
|
|
7945
|
-
const rows = listCronJobs().filter((job) => String(job.command).includes("gosuslugi check"));
|
|
7946
|
-
if (options.json) printJson(rows);
|
|
7947
|
-
else printTable(rows, [["id", "ID"], ["enabled", "Вкл"], ["schedule_text", "Расписание"], ["command", "Команда"], ["last_run_at", "Последний запуск"]]);
|
|
7948
|
-
}
|
|
7949
|
-
|
|
7950
|
-
function parseDurationMs(value) {
|
|
7951
|
-
const text = String(value || "30m").trim().toLocaleLowerCase("ru-RU");
|
|
7952
|
-
const match = text.match(/^(\d+(?:[.,]\d+)?)(ms|s|m|h|мин|минут|час|часа|часов)?$/u);
|
|
7953
|
-
if (!match) throw new Error("Интервал задается как 30m, 1800s или 1h.");
|
|
7954
|
-
const amount = Number(match[1].replace(",", "."));
|
|
7955
|
-
const unit = match[2] || "m";
|
|
7956
|
-
if (unit === "ms") return Math.max(1000, amount);
|
|
7957
|
-
if (unit === "s") return Math.max(1000, amount * 1000);
|
|
7958
|
-
if (unit === "h" || unit.startsWith("час")) return Math.max(1000, amount * 60 * 60 * 1000);
|
|
7959
|
-
return Math.max(1000, amount * 60 * 1000);
|
|
7960
|
-
}
|
|
7961
|
-
|
|
7962
|
-
function printGosuslugiDebt(result) {
|
|
7963
|
-
printKeyValue({
|
|
7964
|
-
total: result.total,
|
|
7965
|
-
amount: `${formatRub(result.amount)} Р`,
|
|
7966
|
-
});
|
|
7967
|
-
if (!result.debts.length) {
|
|
7968
|
-
console.log("Задолженности не найдены.");
|
|
7969
|
-
return;
|
|
7970
|
-
}
|
|
7971
|
-
printTable(result.debts.map((item) => ({
|
|
7972
|
-
group: item.group,
|
|
7973
|
-
amount: `${formatRub(item.amount)} Р`,
|
|
7974
|
-
date: item.billDate,
|
|
7975
|
-
caption: item.caption,
|
|
7976
|
-
})), [
|
|
7977
|
-
["group", "Группа"],
|
|
7978
|
-
["amount", "Сумма"],
|
|
7979
|
-
["date", "Дата"],
|
|
7980
|
-
["caption", "Описание"],
|
|
7981
|
-
]);
|
|
7982
|
-
}
|
|
7983
|
-
|
|
7984
|
-
function printGosuslugiNotifications(result) {
|
|
7985
|
-
printKeyValue({ total: result.total, unread: result.unread, hasMore: result.hasMore ? "yes" : "no" });
|
|
7986
|
-
printTable(result.items.map((item) => ({
|
|
7987
|
-
unread: item.unread ? "new" : "read",
|
|
7988
|
-
date: item.date,
|
|
7989
|
-
type: item.type,
|
|
7990
|
-
title: item.title,
|
|
7991
|
-
subtitle: item.subtitle,
|
|
7992
|
-
summary: item.summary,
|
|
7993
|
-
})), [
|
|
7994
|
-
["unread", "Статус"],
|
|
7995
|
-
["date", "Дата"],
|
|
7996
|
-
["type", "Тип"],
|
|
7997
|
-
["title", "Заголовок"],
|
|
7998
|
-
["subtitle", "Подзаголовок"],
|
|
7999
|
-
["summary", "Детали"],
|
|
8000
|
-
]);
|
|
8001
|
-
}
|
|
8002
|
-
|
|
8003
|
-
function summarizeNotificationData(data) {
|
|
8004
|
-
if (!data || typeof data !== "object") return "";
|
|
8005
|
-
const snippets = Array.isArray(data.snippets) ? data.snippets : [];
|
|
8006
|
-
if (snippets.length) {
|
|
8007
|
-
const first = snippets[0];
|
|
8008
|
-
return [first.orgName, first.address, first.date].filter(Boolean).join(" | ");
|
|
8009
|
-
}
|
|
8010
|
-
return [data.messageType, data.messageUuid, data.orderId, data.passCodeEpguCode].filter(Boolean).join(" | ");
|
|
8011
|
-
}
|
|
8012
|
-
|
|
8013
|
-
function formatRub(value) {
|
|
8014
|
-
return Number(value || 0).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
8015
|
-
}
|
|
8016
|
-
|
|
8017
|
-
function isGosuslugiPersonalIntent(question) {
|
|
8018
|
-
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
8019
|
-
return /(госуслуг|задолж|долг|штраф|налог|к оплате|платеж|платёж|уведомлен|госпочт|фио|дата рождения)/iu.test(normalized);
|
|
8020
|
-
}
|
|
8021
|
-
|
|
8022
|
-
async function answerGosuslugiQuestion(question, options = {}) {
|
|
8023
|
-
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
8024
|
-
if (/(уведомлен|сообщени|госпочт|непрочитан)/iu.test(normalized)) {
|
|
8025
|
-
const result = await gosuslugiNotifications({ unread: /непрочитан|нов/iu.test(normalized), limit: options.limit || 10 });
|
|
8026
|
-
const lines = [`На Госуслугах: всего уведомлений ${result.total}, непрочитанных ${result.unread}.`];
|
|
8027
|
-
const items = result.items.slice(0, Number(options.limit || 5));
|
|
8028
|
-
if (items.length) {
|
|
8029
|
-
lines.push("");
|
|
8030
|
-
for (const item of items) {
|
|
8031
|
-
lines.push(`- ${item.unread ? "новое" : "прочитано"}: ${item.title} — ${item.subtitle} (${item.date})`);
|
|
8032
|
-
}
|
|
8033
|
-
}
|
|
8034
|
-
return lines.join("\n");
|
|
8035
|
-
}
|
|
8036
|
-
if (/(задолж|долг|штраф|налог|к оплате|платеж|платёж)/iu.test(normalized)) {
|
|
8037
|
-
const result = await gosuslugiDebt(options);
|
|
8038
|
-
if (!result.debts.length) return "На Госуслугах задолженности к оплате не найдены.";
|
|
8039
|
-
const lines = [`На Госуслугах найдено задолженностей: ${result.total}. Общая сумма: ${formatRub(result.amount)} Р.`];
|
|
8040
|
-
for (const item of result.debts) {
|
|
8041
|
-
lines.push(`- ${item.group}: ${formatRub(item.amount)} Р — ${item.caption}`);
|
|
8042
|
-
}
|
|
8043
|
-
return lines.join("\n");
|
|
8044
|
-
}
|
|
8045
|
-
const result = await gosuslugiWhoami(options);
|
|
8046
|
-
return [
|
|
8047
|
-
`ФИО: ${result.summary.fio}`,
|
|
8048
|
-
`Дата рождения: ${result.summary.birthDate}`,
|
|
8049
|
-
`Статус: ${result.summary.status}`,
|
|
8050
|
-
].join("\n");
|
|
8051
|
-
}
|
|
8052
|
-
|
|
8053
|
-
function maskPhone(value) {
|
|
8054
|
-
const text = String(value || "");
|
|
8055
|
-
return text.replace(/(\+?\d)([\d\s()-]{4,})(\d{2})$/u, "$1***$3") || "-";
|
|
8056
|
-
}
|
|
8057
|
-
|
|
8058
|
-
function maskEmail(value) {
|
|
8059
|
-
const text = String(value || "");
|
|
8060
|
-
const [name, domain] = text.split("@");
|
|
8061
|
-
if (!name || !domain) return text || "-";
|
|
8062
|
-
return `${name.slice(0, 2)}***@${domain}`;
|
|
8063
|
-
}
|
|
8064
|
-
|
|
8065
|
-
function maskDocument(value) {
|
|
8066
|
-
const digits = String(value || "").replace(/\D+/g, "");
|
|
8067
|
-
if (!digits) return "-";
|
|
8068
|
-
return `***${digits.slice(-4)}`;
|
|
8069
|
-
}
|
|
8070
|
-
|
|
8071
|
-
function redactGosuslugiSensitive(value, options = {}) {
|
|
8072
|
-
if (Array.isArray(value)) return value.map((item) => redactGosuslugiSensitive(item, options));
|
|
8073
|
-
if (!value || typeof value !== "object") return value;
|
|
8074
|
-
const result = {};
|
|
8075
|
-
for (const [key, item] of Object.entries(value)) {
|
|
8076
|
-
if (/token|cookie|session|password|secret|jwt|auth/i.test(key)) result[key] = "[redacted]";
|
|
8077
|
-
else if (!options.keepPersonal && /(snils|inn|passport|number|series|address|mobile|email|phone)/i.test(key)) result[key] = "[redacted]";
|
|
8078
|
-
else result[key] = redactGosuslugiSensitive(item, options);
|
|
8079
|
-
}
|
|
8080
|
-
return result;
|
|
8081
|
-
}
|
|
8082
|
-
|
|
8083
|
-
async function runPersistentBrowserAutomation(action, params) {
|
|
8084
|
-
await ensureBrowserRuntime();
|
|
8085
|
-
await mkdir(params.userDataDir, { recursive: true });
|
|
8086
|
-
const releaseLock = params.userDataDir === GOSUSLUGI_BROWSER_PROFILE_DIR ? await acquireDirectoryLock(GOSUSLUGI_BROWSER_LOCK_DIR, 180000) : async () => {};
|
|
8087
|
-
const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-profile-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
|
|
8088
|
-
await writeFile(scriptFile, persistentBrowserAutomationScript(action, params), "utf8");
|
|
8089
|
-
try {
|
|
8090
|
-
const options = action === "open" ? { cwd: BROWSER_RUNTIME_DIR, inherit: true } : { cwd: BROWSER_RUNTIME_DIR };
|
|
8091
|
-
const result = await runCommand(process.execPath, [scriptFile], options);
|
|
8092
|
-
return result.stdout?.trim() || "";
|
|
8093
|
-
} finally {
|
|
8094
|
-
await rm(scriptFile, { force: true }).catch(() => {});
|
|
8095
|
-
await releaseLock();
|
|
8096
|
-
}
|
|
8097
|
-
}
|
|
8098
|
-
|
|
8099
|
-
async function acquireDirectoryLock(lockDir, timeoutMs = 60000) {
|
|
8100
|
-
const started = Date.now();
|
|
8101
|
-
while (true) {
|
|
8102
|
-
try {
|
|
8103
|
-
await mkdir(lockDir, { recursive: false });
|
|
8104
|
-
await writeFile(path.join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2), "utf8").catch(() => {});
|
|
8105
|
-
return async () => {
|
|
8106
|
-
await rm(lockDir, { recursive: true, force: true }).catch(() => {});
|
|
8107
|
-
};
|
|
8108
|
-
} catch {
|
|
8109
|
-
if (Date.now() - started > timeoutMs) {
|
|
8110
|
-
throw new Error("Браузерный профиль Госуслуг занят другим процессом. Закройте окно Госуслуг или повторите команду позже.");
|
|
8111
|
-
}
|
|
8112
|
-
await sleep(1000);
|
|
8113
|
-
}
|
|
8114
|
-
}
|
|
8115
|
-
}
|
|
8116
|
-
|
|
8117
|
-
async function gosuslugiBrowserApiJson(params) {
|
|
8118
|
-
await requireGosuslugiConsent();
|
|
8119
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
8120
|
-
const raw = await runPersistentBrowserAutomation("api-json", {
|
|
8121
|
-
pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
|
|
8122
|
-
endpoint: params.endpoint,
|
|
8123
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
8124
|
-
headed: params.headed !== false,
|
|
8125
|
-
waitMs: Number(params.waitMs || 0),
|
|
8126
|
-
timeout: Number(params.timeout || 60000),
|
|
8127
|
-
viewport: params.viewport || "1366x768",
|
|
8128
|
-
});
|
|
8129
|
-
return JSON.parse(raw);
|
|
8130
|
-
}
|
|
8131
|
-
|
|
8132
|
-
async function gosuslugiBrowserClickText(params) {
|
|
8133
|
-
await requireGosuslugiConsent();
|
|
8134
|
-
await ensureBrowserRuntimeForGosuslugi();
|
|
8135
|
-
return runPersistentBrowserAutomation("click-text", {
|
|
8136
|
-
pageUrl: params.pageUrl || GOSUSLUGI_DEFAULT_URL,
|
|
8137
|
-
text: params.text,
|
|
8138
|
-
userDataDir: GOSUSLUGI_BROWSER_PROFILE_DIR,
|
|
8139
|
-
headed: true,
|
|
8140
|
-
waitMs: Number(params.waitMs || 3000),
|
|
8141
|
-
timeout: Number(params.timeout || 60000),
|
|
8142
|
-
viewport: params.viewport || "1366x768",
|
|
8143
|
-
});
|
|
8144
|
-
}
|
|
8145
|
-
|
|
8146
|
-
function persistentBrowserAutomationScript(action, params) {
|
|
8147
|
-
return `
|
|
8148
|
-
import { chromium } from "playwright";
|
|
8149
|
-
const action = ${JSON.stringify(action)};
|
|
8150
|
-
const params = ${JSON.stringify(params)};
|
|
8151
|
-
const [width, height] = String(params.viewport || "1366x768").split("x").map(Number);
|
|
8152
|
-
const context = await chromium.launchPersistentContext(params.userDataDir, {
|
|
8153
|
-
headless: !params.headed,
|
|
8154
|
-
viewport: { width: width || 1366, height: height || 768 },
|
|
8155
|
-
});
|
|
8156
|
-
context.setDefaultTimeout(params.timeout || 60000);
|
|
8157
|
-
const page = context.pages()[0] || await context.newPage();
|
|
8158
|
-
try {
|
|
8159
|
-
await page.goto(params.url || params.pageUrl, { waitUntil: "domcontentloaded", timeout: params.timeout || 60000 });
|
|
8160
|
-
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
8161
|
-
if (action === "open") {
|
|
8162
|
-
if (params.headed) {
|
|
8163
|
-
page.on("close", async () => {
|
|
8164
|
-
await context.close().catch(() => {});
|
|
8165
|
-
});
|
|
8166
|
-
while (!page.isClosed()) {
|
|
8167
|
-
await page.waitForTimeout(1000).catch(() => {});
|
|
8168
|
-
}
|
|
8169
|
-
}
|
|
8170
|
-
} else if (action === "text") {
|
|
8171
|
-
console.log((await page.locator("body").innerText()).trim());
|
|
8172
|
-
} else if (action === "screenshot") {
|
|
8173
|
-
await page.screenshot({ path: params.output, fullPage: true });
|
|
8174
|
-
} else if (action === "api-json") {
|
|
8175
|
-
const data = await page.evaluate(async (endpoint) => {
|
|
8176
|
-
const response = await fetch(endpoint, {
|
|
8177
|
-
credentials: "include",
|
|
8178
|
-
headers: { accept: "application/json" },
|
|
8179
|
-
});
|
|
8180
|
-
const text = await response.text();
|
|
8181
|
-
if (!response.ok) throw new Error(response.status + " " + response.statusText + ": " + text.slice(0, 500));
|
|
8182
|
-
return JSON.parse(text);
|
|
8183
|
-
}, params.endpoint);
|
|
8184
|
-
console.log(JSON.stringify(data));
|
|
8185
|
-
} else if (action === "click-text") {
|
|
8186
|
-
await page.getByText(params.text, { exact: true }).first().click();
|
|
8187
|
-
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
8188
|
-
console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
|
|
8189
|
-
}
|
|
8190
|
-
} finally {
|
|
8191
|
-
await context.close().catch(() => {});
|
|
8192
|
-
}
|
|
8193
|
-
`;
|
|
8194
|
-
}
|
|
8195
|
-
|
|
8196
7270
|
function browserAutomationScript(action, params) {
|
|
8197
7271
|
return `
|
|
8198
7272
|
import { chromium } from "playwright";
|
|
@@ -8423,9 +7497,6 @@ function mcpTools() {
|
|
|
8423
7497
|
{ name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
|
|
8424
7498
|
{ name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
|
|
8425
7499
|
{ name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
|
|
8426
|
-
{ name: "gosuslugi.whoami", description: "Прочитать ФИО и дату рождения из личного профиля Госуслуг через локальный браузерный профиль.", inputSchema: schema({ full: { type: "boolean" } }) },
|
|
8427
|
-
{ name: "gosuslugi.debt", description: "Прочитать задолженности и платежи к оплате на Госуслугах.", inputSchema: schema() },
|
|
8428
|
-
{ name: "gosuslugi.notifications", description: "Прочитать уведомления Госуслуг.", inputSchema: schema({ unread: { type: "boolean" }, limit: { type: "number" } }) },
|
|
8429
7500
|
];
|
|
8430
7501
|
}
|
|
8431
7502
|
|
|
@@ -8463,9 +7534,6 @@ async function callMcpTool(name, args = {}) {
|
|
|
8463
7534
|
await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
|
|
8464
7535
|
return { output };
|
|
8465
7536
|
}
|
|
8466
|
-
if (name === "gosuslugi.whoami") return gosuslugiWhoami(args);
|
|
8467
|
-
if (name === "gosuslugi.debt") return gosuslugiDebt(args);
|
|
8468
|
-
if (name === "gosuslugi.notifications") return gosuslugiNotifications(args);
|
|
8469
7537
|
return executeRpc(name, { ...args, _: [] });
|
|
8470
7538
|
}
|
|
8471
7539
|
|
|
@@ -9544,10 +8612,6 @@ function mergeConfig(base, override) {
|
|
|
9544
8612
|
...base.api,
|
|
9545
8613
|
...(override.api || {}),
|
|
9546
8614
|
},
|
|
9547
|
-
gosuslugi: {
|
|
9548
|
-
...base.gosuslugi,
|
|
9549
|
-
...(override.gosuslugi || {}),
|
|
9550
|
-
},
|
|
9551
8615
|
ai: {
|
|
9552
8616
|
...base.ai,
|
|
9553
8617
|
...(override.ai || {}),
|
|
@@ -9630,11 +8694,6 @@ function validateConfig(config) {
|
|
|
9630
8694
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
9631
8695
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
9632
8696
|
}
|
|
9633
|
-
if (config.gosuslugi?.enabled && !isGosuslugiConfigured(config)) {
|
|
9634
|
-
if ((config.gosuslugi?.mode || "personal-browser") !== "personal-browser") {
|
|
9635
|
-
errors.push("gosuslugi включен в OAuth/OIDC-режиме, но authUrl/tokenUrl/clientId не заполнены");
|
|
9636
|
-
}
|
|
9637
|
-
}
|
|
9638
8697
|
return errors;
|
|
9639
8698
|
}
|
|
9640
8699
|
|
|
@@ -9644,7 +8703,6 @@ function configSchema() {
|
|
|
9644
8703
|
required: ["api", "ai"],
|
|
9645
8704
|
properties: {
|
|
9646
8705
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
9647
|
-
gosuslugi: { modes: ["personal-browser", "personal-local"], browserProfile: GOSUSLUGI_BROWSER_PROFILE_DIR, oauthRequiredWhenEnabled: ["authUrl", "tokenUrl", "clientId"], optional: ["userinfoUrl", "clientSecret", "scope", "redirectHost", "redirectPort", "redirectPath"] },
|
|
9648
8706
|
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
9649
8707
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
9650
8708
|
toolsets: { available: Object.keys(TOOLSETS) },
|