@iola_adm/iola-cli 0.1.30 → 0.1.32
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 +6 -0
- package/package.json +1 -1
- package/src/cli.js +474 -2
- package/wiki/Home.md +2 -0
- package/wiki//320/221/321/200/320/260/321/203/320/267/320/265/321/200/320/275/321/213/320/271-/320/260/320/263/320/265/320/275/321/202.md +68 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +8 -0
- 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 +73 -0
package/README.md
CHANGED
|
@@ -74,6 +74,8 @@ iola budget status
|
|
|
74
74
|
iola subagents list
|
|
75
75
|
iola trajectory last
|
|
76
76
|
iola review config
|
|
77
|
+
iola browser status
|
|
78
|
+
iola gosuslugi status
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
Локальная модель через Ollama:
|
|
@@ -100,6 +102,8 @@ iola version --check
|
|
|
100
102
|
- [Локальные файлы](https://github.com/adm-iola/iola-cli/wiki/Локальные-файлы)
|
|
101
103
|
- [Рабочая среда агента](https://github.com/adm-iola/iola-cli/wiki/Рабочая-среда-агента)
|
|
102
104
|
- [Платформа агента](https://github.com/adm-iola/iola-cli/wiki/Платформа-агента)
|
|
105
|
+
- [Браузерный агент](https://github.com/adm-iola/iola-cli/wiki/Браузерный-агент)
|
|
106
|
+
- [Подключение Госуслуг](https://github.com/adm-iola/iola-cli/wiki/Подключение-Госуслуг)
|
|
103
107
|
- [Расширения и локальные данные](https://github.com/adm-iola/iola-cli/wiki/Расширения-и-локальные-данные)
|
|
104
108
|
- [Архивы и мастер настройки](https://github.com/adm-iola/iola-cli/wiki/Архивы-и-мастер-настройки)
|
|
105
109
|
- [Daemon, RPC и cron](https://github.com/adm-iola/iola-cli/wiki/Daemon-RPC-и-cron)
|
|
@@ -116,6 +120,8 @@ iola version --check
|
|
|
116
120
|
- skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
117
121
|
- subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
|
|
118
122
|
- полноценный локальный MCP server по stdio/http: tools, resources и prompts;
|
|
123
|
+
- браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
|
|
124
|
+
- локальный OAuth/OIDC-каркас для подключения Госуслуг/ЕСИА через официальный redirect flow;
|
|
119
125
|
- управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
|
|
120
126
|
- планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
|
|
121
127
|
- экспорт отчетов в Excel/Word-совместимые файлы;
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
3
4
|
import { createServer } from "node:http";
|
|
4
5
|
import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -22,6 +23,8 @@ const DB_SCHEMA_VERSION = 8;
|
|
|
22
23
|
const PROJECT_IOLA_DIR = path.join(process.cwd(), ".iola");
|
|
23
24
|
const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
|
|
24
25
|
const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
|
|
26
|
+
const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
|
|
27
|
+
const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
|
|
25
28
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
26
29
|
const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
|
|
27
30
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
@@ -107,6 +110,18 @@ const DEFAULT_AI_CONFIG = {
|
|
|
107
110
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
108
111
|
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
109
112
|
},
|
|
113
|
+
gosuslugi: {
|
|
114
|
+
enabled: false,
|
|
115
|
+
authUrl: "",
|
|
116
|
+
tokenUrl: "",
|
|
117
|
+
userinfoUrl: "",
|
|
118
|
+
clientId: "",
|
|
119
|
+
clientSecret: "",
|
|
120
|
+
scope: "openid",
|
|
121
|
+
redirectHost: "127.0.0.1",
|
|
122
|
+
redirectPort: 18791,
|
|
123
|
+
redirectPath: "/gosuslugi/callback",
|
|
124
|
+
},
|
|
110
125
|
ai: {
|
|
111
126
|
activeProfile: "local",
|
|
112
127
|
provider: "ollama",
|
|
@@ -260,6 +275,7 @@ const COMMANDS = new Map([
|
|
|
260
275
|
["fork", forkSession],
|
|
261
276
|
["features", handleFeatures],
|
|
262
277
|
["settings", handleSettings],
|
|
278
|
+
["gosuslugi", handleGosuslugi],
|
|
263
279
|
["wiki", handleWiki],
|
|
264
280
|
["context", handleContext],
|
|
265
281
|
["skills", handleSkills],
|
|
@@ -271,6 +287,7 @@ const COMMANDS = new Map([
|
|
|
271
287
|
["index", handleIndex],
|
|
272
288
|
["reports", handleReports],
|
|
273
289
|
["plugins", handlePlugins],
|
|
290
|
+
["browser", handleBrowser],
|
|
274
291
|
["workspace", handleWorkspace],
|
|
275
292
|
["tasks", handleTasks],
|
|
276
293
|
["artifacts", handleArtifacts],
|
|
@@ -385,6 +402,7 @@ Usage:
|
|
|
385
402
|
iola fork SESSION_ID [TEXT]
|
|
386
403
|
iola features list|enable|disable
|
|
387
404
|
iola settings list|get|validate|doctor|init
|
|
405
|
+
iola gosuslugi configure|status|login|logout|userinfo
|
|
388
406
|
iola wiki [open|links]
|
|
389
407
|
iola context list|show|init
|
|
390
408
|
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
@@ -396,6 +414,7 @@ Usage:
|
|
|
396
414
|
iola index folder|status|search
|
|
397
415
|
iola reports list|run
|
|
398
416
|
iola plugins list|install|run|remove
|
|
417
|
+
iola browser status|install|open|text|html|screenshot|pdf|click|type|eval
|
|
399
418
|
iola workspace init|status|use|list
|
|
400
419
|
iola tasks list|add|done|run
|
|
401
420
|
iola artifacts list|show|open
|
|
@@ -680,6 +699,11 @@ async function handleAgentLine(line, state) {
|
|
|
680
699
|
return false;
|
|
681
700
|
}
|
|
682
701
|
|
|
702
|
+
if (command === "gosuslugi") {
|
|
703
|
+
await handleGosuslugi(args);
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
|
|
683
707
|
if (command === "workspace") {
|
|
684
708
|
await handleWorkspace(args);
|
|
685
709
|
return false;
|
|
@@ -829,6 +853,7 @@ async function handleAgentLine(line, state) {
|
|
|
829
853
|
resume: ["resume", args],
|
|
830
854
|
fork: ["fork", args],
|
|
831
855
|
features: ["features", args],
|
|
856
|
+
gosuslugi: ["gosuslugi", args],
|
|
832
857
|
wiki: ["wiki", args],
|
|
833
858
|
context: ["context", args],
|
|
834
859
|
skills: ["skills", args],
|
|
@@ -887,6 +912,7 @@ function printAgentHelp() {
|
|
|
887
912
|
/sessions
|
|
888
913
|
/resume SESSION_ID
|
|
889
914
|
/features list
|
|
915
|
+
/gosuslugi status
|
|
890
916
|
/wiki
|
|
891
917
|
/context list
|
|
892
918
|
/skills list
|
|
@@ -1696,6 +1722,74 @@ async function handleSettings(args) {
|
|
|
1696
1722
|
throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
|
|
1697
1723
|
}
|
|
1698
1724
|
|
|
1725
|
+
async function handleGosuslugi(args) {
|
|
1726
|
+
const [action = "status", ...rest] = args;
|
|
1727
|
+
const options = parseOptions(rest);
|
|
1728
|
+
|
|
1729
|
+
if (action === "status") {
|
|
1730
|
+
const config = await loadConfig();
|
|
1731
|
+
const secrets = await loadSecrets();
|
|
1732
|
+
const tokens = secrets.gosuslugi?.tokens || null;
|
|
1733
|
+
printKeyValue({
|
|
1734
|
+
enabled: config.gosuslugi?.enabled ? "yes" : "no",
|
|
1735
|
+
configured: isGosuslugiConfigured(config) ? "yes" : "no",
|
|
1736
|
+
clientId: config.gosuslugi?.clientId ? maskSecret(config.gosuslugi.clientId) : "-",
|
|
1737
|
+
authUrl: config.gosuslugi?.authUrl || "-",
|
|
1738
|
+
tokenUrl: config.gosuslugi?.tokenUrl || "-",
|
|
1739
|
+
userinfoUrl: config.gosuslugi?.userinfoUrl || "-",
|
|
1740
|
+
redirectUri: gosuslugiRedirectUri(config),
|
|
1741
|
+
connected: tokens?.access_token ? "yes" : "no",
|
|
1742
|
+
savedAt: secrets.gosuslugi?.savedAt || "-",
|
|
1743
|
+
expiresAt: secrets.gosuslugi?.expiresAt || "-",
|
|
1744
|
+
});
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
if (action === "configure") {
|
|
1749
|
+
const current = await loadConfig();
|
|
1750
|
+
const next = {
|
|
1751
|
+
...(current.gosuslugi || {}),
|
|
1752
|
+
enabled: true,
|
|
1753
|
+
authUrl: options["auth-url"] || current.gosuslugi?.authUrl || "",
|
|
1754
|
+
tokenUrl: options["token-url"] || current.gosuslugi?.tokenUrl || "",
|
|
1755
|
+
userinfoUrl: options["userinfo-url"] || current.gosuslugi?.userinfoUrl || "",
|
|
1756
|
+
clientId: options["client-id"] || current.gosuslugi?.clientId || "",
|
|
1757
|
+
clientSecret: options["client-secret"] || current.gosuslugi?.clientSecret || "",
|
|
1758
|
+
scope: options.scope || current.gosuslugi?.scope || "openid",
|
|
1759
|
+
redirectHost: options["redirect-host"] || current.gosuslugi?.redirectHost || "127.0.0.1",
|
|
1760
|
+
redirectPort: Number(options["redirect-port"] || current.gosuslugi?.redirectPort || 18791),
|
|
1761
|
+
redirectPath: options["redirect-path"] || current.gosuslugi?.redirectPath || "/gosuslugi/callback",
|
|
1762
|
+
};
|
|
1763
|
+
await saveConfig({ gosuslugi: next });
|
|
1764
|
+
console.log("Настройки подключения к Госуслугам сохранены.");
|
|
1765
|
+
console.log(`Redirect URI: ${gosuslugiRedirectUri({ gosuslugi: next })}`);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (action === "login") {
|
|
1770
|
+
const result = await gosuslugiLogin(options);
|
|
1771
|
+
printKeyValue(result);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (action === "logout") {
|
|
1776
|
+
const secrets = await loadSecrets();
|
|
1777
|
+
delete secrets.gosuslugi;
|
|
1778
|
+
await saveSecrets(secrets);
|
|
1779
|
+
console.log("Локальное подключение Госуслуг удалено.");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (action === "userinfo" || action === "me") {
|
|
1784
|
+
const result = await gosuslugiUserinfo(options);
|
|
1785
|
+
if (options.json) printJson(result);
|
|
1786
|
+
else printKeyValue(flattenObjectForPrint(result));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
throw new Error("Команды gosuslugi: configure, status, login, logout, userinfo.");
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1699
1793
|
async function handleWiki(args) {
|
|
1700
1794
|
const [action = "links"] = args;
|
|
1701
1795
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -1709,6 +1803,8 @@ async function handleWiki(args) {
|
|
|
1709
1803
|
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1710
1804
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1711
1805
|
["Платформа агента", `${base}/Платформа-агента`],
|
|
1806
|
+
["Браузерный агент", `${base}/Браузерный-агент`],
|
|
1807
|
+
["Подключение Госуслуг", `${base}/Подключение-Госуслуг`],
|
|
1712
1808
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
1713
1809
|
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
1714
1810
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
@@ -2209,6 +2305,94 @@ async function handlePlugins(args) {
|
|
|
2209
2305
|
throw new Error("Команды plugins: list, install NAME --command CMD, run NAME, remove NAME.");
|
|
2210
2306
|
}
|
|
2211
2307
|
|
|
2308
|
+
async function handleBrowser(args) {
|
|
2309
|
+
const [action = "status", target, ...rest] = args;
|
|
2310
|
+
const options = parseOptions(rest);
|
|
2311
|
+
|
|
2312
|
+
if (action === "status") {
|
|
2313
|
+
printKeyValue(await getBrowserStatus());
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
if (action === "install") {
|
|
2318
|
+
await installBrowserRuntime();
|
|
2319
|
+
printKeyValue(await getBrowserStatus());
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
if (action === "open") {
|
|
2324
|
+
const url = target || options.url;
|
|
2325
|
+
if (!url) throw new Error("Пример: iola browser open https://example.com");
|
|
2326
|
+
if (options.system) {
|
|
2327
|
+
await openUrl(url);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
await runBrowserAutomation("open", { url, headed: options.headless ? false : true, waitMs: Number(options.wait || 600000) });
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
if (action === "text" || action === "html") {
|
|
2335
|
+
const url = target || options.url;
|
|
2336
|
+
if (!url) throw new Error(`Пример: iola browser ${action} https://example.com`);
|
|
2337
|
+
const result = await runBrowserAutomation(action, browserParams(url, options));
|
|
2338
|
+
if (options.output) {
|
|
2339
|
+
await writeFile(options.output, result, "utf8");
|
|
2340
|
+
console.log(`Файл сохранен: ${options.output}`);
|
|
2341
|
+
} else {
|
|
2342
|
+
console.log(result);
|
|
2343
|
+
}
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if (action === "screenshot" || action === "pdf") {
|
|
2348
|
+
const url = target || options.url;
|
|
2349
|
+
if (!url) throw new Error(`Пример: iola browser ${action} https://example.com --output page.${action === "pdf" ? "pdf" : "png"}`);
|
|
2350
|
+
const output = options.output || path.join(process.cwd(), action === "pdf" ? "browser-page.pdf" : "browser-page.png");
|
|
2351
|
+
await runBrowserAutomation(action, { ...browserParams(url, options), output: path.resolve(output) });
|
|
2352
|
+
saveArtifact(action === "pdf" ? "browser-pdf" : "browser-screenshot", url, path.resolve(output), { url });
|
|
2353
|
+
console.log(`Файл сохранен: ${output}`);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
if (action === "click") {
|
|
2358
|
+
const url = target || options.url;
|
|
2359
|
+
if (!url || !options.selector) throw new Error('Пример: iola browser click https://example.com --selector "button" --output after.png');
|
|
2360
|
+
const result = await runBrowserAutomation("click", { ...browserParams(url, options), selector: options.selector, output: options.output ? path.resolve(options.output) : "" });
|
|
2361
|
+
if (result) console.log(result);
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
if (action === "type") {
|
|
2366
|
+
const url = target || options.url;
|
|
2367
|
+
if (!url || !options.selector || options.text === undefined) throw new Error('Пример: iola browser type https://example.com --selector "#q" --text "школа 29"');
|
|
2368
|
+
const result = await runBrowserAutomation("type", { ...browserParams(url, options), selector: options.selector, text: options.text, press: options.press || "", output: options.output ? path.resolve(options.output) : "" });
|
|
2369
|
+
if (result) console.log(result);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
if (action === "eval") {
|
|
2374
|
+
const url = target || options.url;
|
|
2375
|
+
const script = options.script || rest.join(" ");
|
|
2376
|
+
if (!url || !script) throw new Error('Пример: iola browser eval https://example.com --script "document.title"');
|
|
2377
|
+
const result = await runBrowserAutomation("eval", { ...browserParams(url, options), script });
|
|
2378
|
+
console.log(result);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
throw new Error("Команды browser: status, install, open URL, text URL, html URL, screenshot URL --output FILE, pdf URL --output FILE, click URL --selector SEL, type URL --selector SEL --text TEXT, eval URL --script JS.");
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
function browserParams(url, options = {}) {
|
|
2386
|
+
return {
|
|
2387
|
+
url,
|
|
2388
|
+
headed: Boolean(options.headed),
|
|
2389
|
+
timeout: Number(options.timeout || 30000),
|
|
2390
|
+
waitMs: Number(options.wait || 0),
|
|
2391
|
+
selector: options.selector || "",
|
|
2392
|
+
viewport: options.viewport || "1366x768",
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2212
2396
|
async function handleWorkspace(args) {
|
|
2213
2397
|
const [action = "status", nameOrPath] = args;
|
|
2214
2398
|
const config = await loadConfig();
|
|
@@ -2547,6 +2731,177 @@ async function openUrl(url) {
|
|
|
2547
2731
|
await runCommand("xdg-open", [url], { inherit: false });
|
|
2548
2732
|
}
|
|
2549
2733
|
|
|
2734
|
+
async function gosuslugiLogin(options = {}) {
|
|
2735
|
+
const config = await loadConfig();
|
|
2736
|
+
if (!isGosuslugiConfigured(config)) {
|
|
2737
|
+
throw new Error("Подключение не настроено. Пример: iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid");
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
const state = randomUrlSafe(24);
|
|
2741
|
+
const codeVerifier = randomUrlSafe(64);
|
|
2742
|
+
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
2743
|
+
const redirectUri = gosuslugiRedirectUri(config);
|
|
2744
|
+
const callback = waitForOAuthCallback(config.gosuslugi, state, Number(options.timeout || 180000));
|
|
2745
|
+
const authUrl = new URL(config.gosuslugi.authUrl);
|
|
2746
|
+
authUrl.searchParams.set("response_type", "code");
|
|
2747
|
+
authUrl.searchParams.set("client_id", config.gosuslugi.clientId);
|
|
2748
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
2749
|
+
authUrl.searchParams.set("scope", config.gosuslugi.scope || "openid");
|
|
2750
|
+
authUrl.searchParams.set("state", state);
|
|
2751
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
2752
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
2753
|
+
|
|
2754
|
+
console.log("Открываю официальный экран входа Госуслуг/ЕСИА в браузере.");
|
|
2755
|
+
console.log("После входа CLI примет callback на локальном адресе и сохранит токены только на этом компьютере.");
|
|
2756
|
+
await openUrl(authUrl.toString());
|
|
2757
|
+
const params = await callback;
|
|
2758
|
+
if (params.error) throw new Error(`Госуслуги вернули ошибку: ${params.error} ${params.error_description || ""}`.trim());
|
|
2759
|
+
if (!params.code) throw new Error("Authorization code не получен.");
|
|
2760
|
+
|
|
2761
|
+
const tokens = await exchangeGosuslugiCode(config, {
|
|
2762
|
+
code: params.code,
|
|
2763
|
+
codeVerifier,
|
|
2764
|
+
redirectUri,
|
|
2765
|
+
});
|
|
2766
|
+
const secrets = await loadSecrets();
|
|
2767
|
+
const now = new Date();
|
|
2768
|
+
const expiresAt = tokens.expires_in ? new Date(now.getTime() + Number(tokens.expires_in) * 1000).toISOString() : "";
|
|
2769
|
+
secrets.gosuslugi = {
|
|
2770
|
+
savedAt: now.toISOString(),
|
|
2771
|
+
expiresAt,
|
|
2772
|
+
tokens,
|
|
2773
|
+
};
|
|
2774
|
+
await saveSecrets(secrets);
|
|
2775
|
+
return {
|
|
2776
|
+
connected: "yes",
|
|
2777
|
+
savedAt: secrets.gosuslugi.savedAt,
|
|
2778
|
+
expiresAt: expiresAt || "-",
|
|
2779
|
+
tokenType: tokens.token_type || "-",
|
|
2780
|
+
scope: tokens.scope || config.gosuslugi.scope || "-",
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
function waitForOAuthCallback(settings, expectedState, timeoutMs) {
|
|
2785
|
+
const host = settings.redirectHost || "127.0.0.1";
|
|
2786
|
+
const port = Number(settings.redirectPort || 18791);
|
|
2787
|
+
const callbackPath = settings.redirectPath || "/gosuslugi/callback";
|
|
2788
|
+
return new Promise((resolve, reject) => {
|
|
2789
|
+
const timer = setTimeout(() => {
|
|
2790
|
+
server.close(() => {});
|
|
2791
|
+
reject(new Error("Истекло время ожидания входа через Госуслуги."));
|
|
2792
|
+
}, timeoutMs);
|
|
2793
|
+
const server = createServer((req, res) => {
|
|
2794
|
+
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
2795
|
+
if (url.pathname !== callbackPath) {
|
|
2796
|
+
res.statusCode = 404;
|
|
2797
|
+
res.end("Not found");
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
2801
|
+
if (params.state !== expectedState) {
|
|
2802
|
+
res.statusCode = 400;
|
|
2803
|
+
res.end("Invalid state");
|
|
2804
|
+
clearTimeout(timer);
|
|
2805
|
+
server.close(() => {});
|
|
2806
|
+
reject(new Error("OAuth state не совпал. Вход отменен."));
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
2810
|
+
res.end("<!doctype html><meta charset=\"utf-8\"><title>iola</title><body>Вход выполнен. Можно закрыть это окно и вернуться в терминал.</body>");
|
|
2811
|
+
clearTimeout(timer);
|
|
2812
|
+
server.close(() => resolve(params));
|
|
2813
|
+
});
|
|
2814
|
+
server.once("error", (error) => {
|
|
2815
|
+
clearTimeout(timer);
|
|
2816
|
+
reject(error);
|
|
2817
|
+
});
|
|
2818
|
+
server.listen(port, host);
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
async function exchangeGosuslugiCode(config, { code, codeVerifier, redirectUri }) {
|
|
2823
|
+
const body = new URLSearchParams();
|
|
2824
|
+
body.set("grant_type", "authorization_code");
|
|
2825
|
+
body.set("code", code);
|
|
2826
|
+
body.set("redirect_uri", redirectUri);
|
|
2827
|
+
body.set("client_id", config.gosuslugi.clientId);
|
|
2828
|
+
body.set("code_verifier", codeVerifier);
|
|
2829
|
+
if (config.gosuslugi.clientSecret) body.set("client_secret", config.gosuslugi.clientSecret);
|
|
2830
|
+
|
|
2831
|
+
const response = await fetch(config.gosuslugi.tokenUrl, {
|
|
2832
|
+
method: "POST",
|
|
2833
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
2834
|
+
body,
|
|
2835
|
+
});
|
|
2836
|
+
const text = await response.text();
|
|
2837
|
+
let payload = {};
|
|
2838
|
+
try {
|
|
2839
|
+
payload = text ? JSON.parse(text) : {};
|
|
2840
|
+
} catch {
|
|
2841
|
+
payload = { raw: text };
|
|
2842
|
+
}
|
|
2843
|
+
if (!response.ok) {
|
|
2844
|
+
throw new Error(`Token endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
2845
|
+
}
|
|
2846
|
+
return payload;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
async function gosuslugiUserinfo() {
|
|
2850
|
+
const config = await loadConfig();
|
|
2851
|
+
const secrets = await loadSecrets();
|
|
2852
|
+
const accessToken = secrets.gosuslugi?.tokens?.access_token;
|
|
2853
|
+
if (!accessToken) throw new Error("Госуслуги не подключены. Запустите: iola gosuslugi login");
|
|
2854
|
+
if (!config.gosuslugi?.userinfoUrl) throw new Error("userinfoUrl не настроен.");
|
|
2855
|
+
const response = await fetch(config.gosuslugi.userinfoUrl, {
|
|
2856
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
|
2857
|
+
});
|
|
2858
|
+
const text = await response.text();
|
|
2859
|
+
let payload = {};
|
|
2860
|
+
try {
|
|
2861
|
+
payload = text ? JSON.parse(text) : {};
|
|
2862
|
+
} catch {
|
|
2863
|
+
payload = { raw: text };
|
|
2864
|
+
}
|
|
2865
|
+
if (!response.ok) throw new Error(`Userinfo endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
2866
|
+
return payload;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
function isGosuslugiConfigured(config) {
|
|
2870
|
+
return Boolean(config.gosuslugi?.authUrl && config.gosuslugi?.tokenUrl && config.gosuslugi?.clientId);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
function gosuslugiRedirectUri(config) {
|
|
2874
|
+
const settings = config.gosuslugi || DEFAULT_AI_CONFIG.gosuslugi;
|
|
2875
|
+
return `http://${settings.redirectHost || "127.0.0.1"}:${Number(settings.redirectPort || 18791)}${settings.redirectPath || "/gosuslugi/callback"}`;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
function randomUrlSafe(bytes) {
|
|
2879
|
+
return base64Url(randomBytes(bytes));
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
function base64Url(buffer) {
|
|
2883
|
+
return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function maskSecret(value) {
|
|
2887
|
+
const text = String(value || "");
|
|
2888
|
+
if (text.length <= 8) return text ? "***" : "-";
|
|
2889
|
+
return `${text.slice(0, 4)}...${text.slice(-4)}`;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function flattenObjectForPrint(value, prefix = "") {
|
|
2893
|
+
const rows = {};
|
|
2894
|
+
for (const [key, item] of Object.entries(value || {})) {
|
|
2895
|
+
const name = prefix ? `${prefix}.${key}` : key;
|
|
2896
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
2897
|
+
Object.assign(rows, flattenObjectForPrint(item, name));
|
|
2898
|
+
} else {
|
|
2899
|
+
rows[name] = Array.isArray(item) ? item.join(", ") : item;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return rows;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2550
2905
|
async function handlePermissions(args) {
|
|
2551
2906
|
const [action = "list", name] = args;
|
|
2552
2907
|
const config = await loadConfig();
|
|
@@ -5903,12 +6258,12 @@ function parseOptions(args) {
|
|
|
5903
6258
|
|
|
5904
6259
|
for (let index = 0; index < args.length; index += 1) {
|
|
5905
6260
|
const arg = args[index];
|
|
5906
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || 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") {
|
|
6261
|
+
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") {
|
|
5907
6262
|
result[arg.slice(2)] = true;
|
|
5908
6263
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
5909
6264
|
result.check = true;
|
|
5910
6265
|
result[arg.slice(2)] = true;
|
|
5911
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--debug-file") {
|
|
6266
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file") {
|
|
5912
6267
|
result[arg.slice(2)] = args[index + 1];
|
|
5913
6268
|
index += 1;
|
|
5914
6269
|
} else {
|
|
@@ -6285,6 +6640,105 @@ async function installCodexIfMissing() {
|
|
|
6285
6640
|
await runCommand("npm", ["install", "-g", "@openai/codex"], { inherit: true });
|
|
6286
6641
|
}
|
|
6287
6642
|
|
|
6643
|
+
async function getBrowserStatus() {
|
|
6644
|
+
const installed = existsSync(BROWSER_RUNTIME_PACKAGE);
|
|
6645
|
+
let playwright = "не установлен";
|
|
6646
|
+
if (installed) {
|
|
6647
|
+
try {
|
|
6648
|
+
playwright = JSON.parse(await readFile(BROWSER_RUNTIME_PACKAGE, "utf8")).version || "installed";
|
|
6649
|
+
} catch {
|
|
6650
|
+
playwright = "installed";
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6653
|
+
return {
|
|
6654
|
+
runtime: BROWSER_RUNTIME_DIR,
|
|
6655
|
+
playwright,
|
|
6656
|
+
installed: installed ? "yes" : "no",
|
|
6657
|
+
install_command: "iola browser install",
|
|
6658
|
+
chromium: installed ? "managed by Playwright" : "not installed",
|
|
6659
|
+
};
|
|
6660
|
+
}
|
|
6661
|
+
|
|
6662
|
+
async function installBrowserRuntime() {
|
|
6663
|
+
await mkdir(BROWSER_RUNTIME_DIR, { recursive: true });
|
|
6664
|
+
const packageFile = path.join(BROWSER_RUNTIME_DIR, "package.json");
|
|
6665
|
+
if (!existsSync(packageFile)) {
|
|
6666
|
+
await writeFile(packageFile, `${JSON.stringify({ private: true, type: "module", dependencies: {} }, null, 2)}\n`, "utf8");
|
|
6667
|
+
}
|
|
6668
|
+
console.log(`Устанавливаю Playwright runtime: ${BROWSER_RUNTIME_DIR}`);
|
|
6669
|
+
await runPackageManager("npm", ["install", "playwright@latest"], { inherit: true, cwd: BROWSER_RUNTIME_DIR });
|
|
6670
|
+
await runPackageManager("npx", ["playwright", "install", "chromium"], { inherit: true, cwd: BROWSER_RUNTIME_DIR });
|
|
6671
|
+
}
|
|
6672
|
+
|
|
6673
|
+
function runPackageManager(command, args, options = {}) {
|
|
6674
|
+
if (process.platform === "win32") {
|
|
6675
|
+
return runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/c", [command, ...args].join(" ")], options);
|
|
6676
|
+
}
|
|
6677
|
+
return runCommand(command, args, options);
|
|
6678
|
+
}
|
|
6679
|
+
|
|
6680
|
+
async function ensureBrowserRuntime() {
|
|
6681
|
+
if (existsSync(BROWSER_RUNTIME_PACKAGE)) return;
|
|
6682
|
+
throw new Error("Browser runtime не установлен. Запустите: iola browser install");
|
|
6683
|
+
}
|
|
6684
|
+
|
|
6685
|
+
async function runBrowserAutomation(action, params) {
|
|
6686
|
+
await ensureBrowserRuntime();
|
|
6687
|
+
const scriptFile = path.join(BROWSER_RUNTIME_DIR, `iola-browser-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
|
|
6688
|
+
await writeFile(scriptFile, browserAutomationScript(action, params), "utf8");
|
|
6689
|
+
try {
|
|
6690
|
+
const { stdout } = await runCommand(process.execPath, [scriptFile], { cwd: BROWSER_RUNTIME_DIR });
|
|
6691
|
+
return stdout.trim();
|
|
6692
|
+
} finally {
|
|
6693
|
+
await rm(scriptFile, { force: true }).catch(() => {});
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
|
|
6697
|
+
function browserAutomationScript(action, params) {
|
|
6698
|
+
return `
|
|
6699
|
+
import { chromium } from "playwright";
|
|
6700
|
+
const action = ${JSON.stringify(action)};
|
|
6701
|
+
const params = ${JSON.stringify(params)};
|
|
6702
|
+
const [width, height] = String(params.viewport || "1366x768").split("x").map(Number);
|
|
6703
|
+
const browser = await chromium.launch({ headless: !params.headed });
|
|
6704
|
+
const page = await browser.newPage({ viewport: { width: width || 1366, height: height || 768 } });
|
|
6705
|
+
page.setDefaultTimeout(params.timeout || 30000);
|
|
6706
|
+
try {
|
|
6707
|
+
await page.goto(params.url, { waitUntil: "domcontentloaded", timeout: params.timeout || 30000 });
|
|
6708
|
+
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
6709
|
+
if (action === "open") {
|
|
6710
|
+
if (params.waitMs > 0) await page.waitForTimeout(params.waitMs);
|
|
6711
|
+
else if (!page.context().browser()?.isConnected()) {}
|
|
6712
|
+
} else if (action === "text") {
|
|
6713
|
+
console.log((await page.locator("body").innerText()).trim());
|
|
6714
|
+
} else if (action === "html") {
|
|
6715
|
+
console.log(await page.content());
|
|
6716
|
+
} else if (action === "screenshot") {
|
|
6717
|
+
await page.screenshot({ path: params.output, fullPage: true });
|
|
6718
|
+
} else if (action === "pdf") {
|
|
6719
|
+
await page.pdf({ path: params.output, format: "A4", printBackground: true });
|
|
6720
|
+
} else if (action === "click") {
|
|
6721
|
+
await page.locator(params.selector).first().click();
|
|
6722
|
+
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
6723
|
+
if (params.output) await page.screenshot({ path: params.output, fullPage: true });
|
|
6724
|
+
console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
|
|
6725
|
+
} else if (action === "type") {
|
|
6726
|
+
const locator = page.locator(params.selector).first();
|
|
6727
|
+
await locator.fill(params.text || "");
|
|
6728
|
+
if (params.press) await locator.press(params.press);
|
|
6729
|
+
if (params.waitMs) await page.waitForTimeout(params.waitMs);
|
|
6730
|
+
if (params.output) await page.screenshot({ path: params.output, fullPage: true });
|
|
6731
|
+
console.log((await page.locator("body").innerText()).trim().slice(0, 4000));
|
|
6732
|
+
} else if (action === "eval") {
|
|
6733
|
+
const value = await page.evaluate(new Function("return (" + params.script + ")"));
|
|
6734
|
+
console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
6735
|
+
}
|
|
6736
|
+
} finally {
|
|
6737
|
+
await browser.close();
|
|
6738
|
+
}
|
|
6739
|
+
`;
|
|
6740
|
+
}
|
|
6741
|
+
|
|
6288
6742
|
async function probeEndpoint(url) {
|
|
6289
6743
|
try {
|
|
6290
6744
|
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
@@ -6468,6 +6922,8 @@ function mcpTools() {
|
|
|
6468
6922
|
{ name: "files.search", description: "Поиск текста в файлах workspace.", inputSchema: schema({ query: { type: "string" }, path: { type: "string" }, limit: { type: "number" } }) },
|
|
6469
6923
|
{ name: "index.search", description: "Поиск по индексу локальных документов.", inputSchema: schema({ query: { type: "string" }, limit: { type: "number" } }) },
|
|
6470
6924
|
{ name: "report", description: "Запуск встроенного отчета.", inputSchema: schema({ name: { type: "string" }, format: { type: "string" }, output: { type: "string" } }) },
|
|
6925
|
+
{ name: "browser.text", description: "Открыть страницу в headless Chromium и вернуть видимый текст.", inputSchema: schema({ url: { type: "string" }, waitMs: { type: "number" } }) },
|
|
6926
|
+
{ name: "browser.screenshot", description: "Сделать скриншот страницы через Chromium.", inputSchema: schema({ url: { type: "string" }, output: { type: "string" }, waitMs: { type: "number" } }) },
|
|
6471
6927
|
];
|
|
6472
6928
|
}
|
|
6473
6929
|
|
|
@@ -6497,6 +6953,14 @@ async function callMcpTool(name, args = {}) {
|
|
|
6497
6953
|
await handleExport([args.name || "education-contacts", "--format", args.format || "xlsx", "--output", output]);
|
|
6498
6954
|
return { output };
|
|
6499
6955
|
}
|
|
6956
|
+
if (name === "browser.text") {
|
|
6957
|
+
return runBrowserAutomation("text", { url: args.url, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
|
|
6958
|
+
}
|
|
6959
|
+
if (name === "browser.screenshot") {
|
|
6960
|
+
const output = path.resolve(args.output || "browser-page.png");
|
|
6961
|
+
await runBrowserAutomation("screenshot", { url: args.url, output, waitMs: Number(args.waitMs || 0), timeout: Number(args.timeout || 30000), viewport: args.viewport || "1366x768" });
|
|
6962
|
+
return { output };
|
|
6963
|
+
}
|
|
6500
6964
|
return executeRpc(name, { ...args, _: [] });
|
|
6501
6965
|
}
|
|
6502
6966
|
|
|
@@ -7567,6 +8031,10 @@ function mergeConfig(base, override) {
|
|
|
7567
8031
|
...base.api,
|
|
7568
8032
|
...(override.api || {}),
|
|
7569
8033
|
},
|
|
8034
|
+
gosuslugi: {
|
|
8035
|
+
...base.gosuslugi,
|
|
8036
|
+
...(override.gosuslugi || {}),
|
|
8037
|
+
},
|
|
7570
8038
|
ai: {
|
|
7571
8039
|
...base.ai,
|
|
7572
8040
|
...(override.ai || {}),
|
|
@@ -7641,6 +8109,9 @@ function validateConfig(config) {
|
|
|
7641
8109
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
7642
8110
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
7643
8111
|
}
|
|
8112
|
+
if (config.gosuslugi?.enabled && !isGosuslugiConfigured(config)) {
|
|
8113
|
+
errors.push("gosuslugi включен, но authUrl/tokenUrl/clientId не заполнены");
|
|
8114
|
+
}
|
|
7644
8115
|
return errors;
|
|
7645
8116
|
}
|
|
7646
8117
|
|
|
@@ -7650,6 +8121,7 @@ function configSchema() {
|
|
|
7650
8121
|
required: ["api", "ai"],
|
|
7651
8122
|
properties: {
|
|
7652
8123
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
8124
|
+
gosuslugi: { requiredWhenEnabled: ["authUrl", "tokenUrl", "clientId"], optional: ["userinfoUrl", "clientSecret", "scope", "redirectHost", "redirectPort", "redirectPath"] },
|
|
7653
8125
|
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
7654
8126
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
7655
8127
|
toolsets: { available: Object.keys(TOOLSETS) },
|
package/wiki/Home.md
CHANGED
|
@@ -32,6 +32,8 @@ iola ask "найди школу 29"
|
|
|
32
32
|
- [Локальные файлы](Локальные-файлы)
|
|
33
33
|
- [Рабочая среда агента](Рабочая-среда-агента)
|
|
34
34
|
- [Платформа агента](Платформа-агента)
|
|
35
|
+
- [Браузерный агент](Браузерный-агент)
|
|
36
|
+
- [Подключение Госуслуг](Подключение-Госуслуг)
|
|
35
37
|
- [Расширения и локальные данные](Расширения-и-локальные-данные)
|
|
36
38
|
- [Архивы и мастер настройки](Архивы-и-мастер-настройки)
|
|
37
39
|
- [Daemon, RPC и cron](Daemon-RPC-и-cron)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Браузерный агент
|
|
2
|
+
|
|
3
|
+
`iola-cli` умеет подключать локальный браузерный runtime на Playwright. Сам npm-пакет остается легким: Chromium и Playwright ставятся отдельно при первом включении браузера.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
iola browser status
|
|
9
|
+
iola browser install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Runtime хранится в `~/.iola/browser-runtime`.
|
|
13
|
+
|
|
14
|
+
## Базовые команды
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
iola browser open https://example.com
|
|
18
|
+
iola browser text https://example.com
|
|
19
|
+
iola browser html https://example.com --output page.html
|
|
20
|
+
iola browser screenshot https://example.com --output page.png
|
|
21
|
+
iola browser pdf https://example.com --output page.pdf
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`open` запускает видимый Chromium и держит его открытым. По умолчанию окно живет 10 минут. Можно задать время:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
iola browser open https://example.com --wait 30000
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Действия на странице
|
|
31
|
+
|
|
32
|
+
Клик:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
iola browser click https://example.com --selector "button" --output after-click.png
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Ввод:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
iola browser type https://example.com --selector "#q" --text "школа 29" --press Enter --wait 2000 --output search.png
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Выполнение JavaScript-выражения:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
iola browser eval https://example.com --script "document.title"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## MCP
|
|
51
|
+
|
|
52
|
+
Локальный MCP server также отдает браузерные tools:
|
|
53
|
+
|
|
54
|
+
- `browser.text`;
|
|
55
|
+
- `browser.screenshot`.
|
|
56
|
+
|
|
57
|
+
Перед использованием этих tools нужно один раз выполнить:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
iola browser install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Режимы
|
|
64
|
+
|
|
65
|
+
- Для чтения страниц используйте `text`, `html`, `screenshot`, `pdf`.
|
|
66
|
+
- Для действий используйте `click` и `type`.
|
|
67
|
+
- Для ручной авторизации или просмотра используйте `open`.
|
|
68
|
+
|
|
@@ -74,6 +74,10 @@ iola import file data.csv --dataset custom
|
|
|
74
74
|
iola index folder ./docs
|
|
75
75
|
iola reports list
|
|
76
76
|
iola plugins list
|
|
77
|
+
iola browser status
|
|
78
|
+
iola browser install
|
|
79
|
+
iola browser text https://example.com
|
|
80
|
+
iola browser screenshot https://example.com --output page.png
|
|
77
81
|
iola mcp serve
|
|
78
82
|
iola mcp serve --stdio
|
|
79
83
|
iola memory suggest
|
|
@@ -88,6 +92,10 @@ iola context init
|
|
|
88
92
|
iola context list
|
|
89
93
|
iola settings list
|
|
90
94
|
iola settings validate
|
|
95
|
+
iola gosuslugi status
|
|
96
|
+
iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid
|
|
97
|
+
iola gosuslugi login
|
|
98
|
+
iola gosuslugi userinfo --json
|
|
91
99
|
iola cron add "каждый день 09:00 -- quality"
|
|
92
100
|
iola cron tick
|
|
93
101
|
iola daemon status
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Подключение Госуслуг
|
|
2
|
+
|
|
3
|
+
`iola-cli` поддерживает локальный OAuth/OIDC-каркас для подключения учетной записи через официальный flow ЕСИА/Госуслуг.
|
|
4
|
+
|
|
5
|
+
Важно: CLI не извлекает cookies, токены или session storage из браузера. Пользователь входит на официальном экране ЕСИА, после чего ЕСИА возвращает `authorization code` на локальный `redirect_uri`, а CLI обменивает его на токены через `token endpoint`.
|
|
6
|
+
|
|
7
|
+
## Настройка
|
|
8
|
+
|
|
9
|
+
Для работы нужны параметры официально подключенной информационной системы:
|
|
10
|
+
|
|
11
|
+
- authorization endpoint;
|
|
12
|
+
- token endpoint;
|
|
13
|
+
- client ID;
|
|
14
|
+
- разрешенный redirect URI;
|
|
15
|
+
- scope;
|
|
16
|
+
- при необходимости client secret или иной официальный способ подписи/аутентификации клиента;
|
|
17
|
+
- optional userinfo endpoint.
|
|
18
|
+
|
|
19
|
+
Команда настройки:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
iola gosuslugi configure \
|
|
23
|
+
--auth-url "https://..." \
|
|
24
|
+
--token-url "https://..." \
|
|
25
|
+
--userinfo-url "https://..." \
|
|
26
|
+
--client-id "CLIENT_ID" \
|
|
27
|
+
--scope "openid" \
|
|
28
|
+
--redirect-port 18791
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
CLI покажет redirect URI:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
http://127.0.0.1:18791/gosuslugi/callback
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Этот URI должен быть разрешен в настройках подключенной информационной системы.
|
|
38
|
+
|
|
39
|
+
## Вход
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
iola gosuslugi login
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Что происходит:
|
|
46
|
+
|
|
47
|
+
1. CLI запускает локальный callback server на `127.0.0.1`.
|
|
48
|
+
2. Открывается официальный экран входа ЕСИА/Госуслуг.
|
|
49
|
+
3. Пользователь сам вводит логин, пароль, SMS/2FA.
|
|
50
|
+
4. ЕСИА возвращает `authorization code` на локальный callback.
|
|
51
|
+
5. CLI обменивает code на токены и сохраняет их локально.
|
|
52
|
+
|
|
53
|
+
Токены хранятся в `~/.iola/secrets.json` на компьютере пользователя.
|
|
54
|
+
|
|
55
|
+
## Проверка
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
iola gosuslugi status
|
|
59
|
+
iola gosuslugi userinfo --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Выход
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
iola gosuslugi logout
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Команда удаляет локально сохраненные токены.
|
|
69
|
+
|
|
70
|
+
## Ограничения
|
|
71
|
+
|
|
72
|
+
Без официальных параметров ЕСИА команда `login` не сможет завершить подключение. Это сделано намеренно: CLI реализует легальный redirect flow, а не копирование живой браузерной сессии.
|
|
73
|
+
|