@iola_adm/iola-cli 0.2.16 → 0.2.18
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 +2 -2
- package/package.json +1 -1
- package/src/cli.js +141 -60
- package/test/smoke-test.js +6 -0
- package/wiki/Yandex-Connector.md +28 -12
package/README.md
CHANGED
|
@@ -197,9 +197,9 @@ iola yandex menu
|
|
|
197
197
|
iola yandex status
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
Yandex Connector использует две встроенные OAuth-группы: `IOLA CLI A` для Yandex ID, Диска, Почты и документов через Диск; `IOLA CLI B` для Календаря, Контактов и Телемоста через календарь. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
|
|
201
201
|
|
|
202
|
-
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. OAuth-права сами по себе не создают функциональность: под каждый сервис нужны отдельные команды и тулы.
|
|
202
|
+
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. Мастер считает коннектор готовым только после токенов обеих групп. OAuth-права сами по себе не создают функциональность: под каждый сервис нужны отдельные команды и тулы.
|
|
203
203
|
|
|
204
204
|
Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
|
|
205
205
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -42,8 +42,8 @@ const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "
|
|
|
42
42
|
const CLOUD_DEFAULT_REMOTE_DIR = "/IOLA";
|
|
43
43
|
const YANDEX_OAUTH_AUTHORIZE_URL = "https://oauth.yandex.ru/authorize";
|
|
44
44
|
const YANDEX_OAUTH_REDIRECT_URL = "https://oauth.yandex.ru/verification_code";
|
|
45
|
-
const YANDEX_CONNECTOR_CLIENT_ID = process.env.IOLA_YANDEX_OAUTH_CLIENT_ID || process.env.YANDEX_OAUTH_CLIENT_ID || "
|
|
46
|
-
const
|
|
45
|
+
const YANDEX_CONNECTOR_CLIENT_ID = process.env.IOLA_YANDEX_OAUTH_CLIENT_ID || process.env.YANDEX_OAUTH_CLIENT_ID || "9b7c9bcf81e8491f9bd36ba44fa76128";
|
|
46
|
+
const YANDEX_CONNECTOR_ORGANIZER_CLIENT_ID = process.env.IOLA_YANDEX_ORGANIZER_OAUTH_CLIENT_ID || "4ab53d8557e64ac98534ed60295cb138";
|
|
47
47
|
const YANDEX_CONNECTOR_REDIRECT_HOST = "127.0.0.1";
|
|
48
48
|
const YANDEX_CONNECTOR_REDIRECT_PORT = Number(process.env.IOLA_YANDEX_OAUTH_PORT || 18791);
|
|
49
49
|
const YANDEX_CONNECTOR_REDIRECT_PATH = "/yandex/oauth/callback";
|
|
@@ -79,30 +79,9 @@ const YANDEX_CONNECTOR_SERVICES = {
|
|
|
79
79
|
contacts: {
|
|
80
80
|
title: "Яндекс Контакты",
|
|
81
81
|
category: "contacts",
|
|
82
|
-
scope: "
|
|
82
|
+
scope: "addressbook:all",
|
|
83
83
|
status: "research",
|
|
84
|
-
hint: "
|
|
85
|
-
},
|
|
86
|
-
wiki: {
|
|
87
|
-
title: "Yandex Wiki",
|
|
88
|
-
category: "workspace",
|
|
89
|
-
scope: "wiki:read wiki:write",
|
|
90
|
-
status: "research",
|
|
91
|
-
hint: "страницы wiki, больше полезно организациям",
|
|
92
|
-
},
|
|
93
|
-
tracker: {
|
|
94
|
-
title: "Yandex Tracker",
|
|
95
|
-
category: "workspace",
|
|
96
|
-
scope: "tracker:read tracker:write",
|
|
97
|
-
status: "research",
|
|
98
|
-
hint: "задачи и обращения, больше полезно организациям",
|
|
99
|
-
},
|
|
100
|
-
forms: {
|
|
101
|
-
title: "Yandex Forms",
|
|
102
|
-
category: "forms",
|
|
103
|
-
scope: "forms:read forms:write",
|
|
104
|
-
status: "research",
|
|
105
|
-
hint: "формы и опросы, API надо подтвердить",
|
|
84
|
+
hint: "адресная книга и контакты, требует проверки API",
|
|
106
85
|
},
|
|
107
86
|
docs: {
|
|
108
87
|
title: "Яндекс Документы / 360",
|
|
@@ -114,9 +93,9 @@ const YANDEX_CONNECTOR_SERVICES = {
|
|
|
114
93
|
telemost: {
|
|
115
94
|
title: "Яндекс Телемост",
|
|
116
95
|
category: "meetings",
|
|
117
|
-
scope: "",
|
|
96
|
+
scope: "calendar:all",
|
|
118
97
|
status: "research",
|
|
119
|
-
hint: "
|
|
98
|
+
hint: "встречи через календарное событие, если поддерживается",
|
|
120
99
|
},
|
|
121
100
|
cloud: {
|
|
122
101
|
title: "Yandex Cloud",
|
|
@@ -157,15 +136,15 @@ const YANDEX_CONNECTOR_SERVICES = {
|
|
|
157
136
|
const YANDEX_CONNECTOR_OAUTH_APPS = [
|
|
158
137
|
{
|
|
159
138
|
id: "core",
|
|
160
|
-
title: "IOLA
|
|
139
|
+
title: "IOLA CLI A",
|
|
161
140
|
clientId: YANDEX_CONNECTOR_CLIENT_ID,
|
|
162
|
-
services: ["identity", "disk", "mail"],
|
|
141
|
+
services: ["identity", "disk", "mail", "docs"],
|
|
163
142
|
},
|
|
164
143
|
{
|
|
165
|
-
id: "
|
|
166
|
-
title: "IOLA
|
|
167
|
-
clientId:
|
|
168
|
-
services: ["
|
|
144
|
+
id: "organizer",
|
|
145
|
+
title: "IOLA CLI B",
|
|
146
|
+
clientId: YANDEX_CONNECTOR_ORGANIZER_CLIENT_ID,
|
|
147
|
+
services: ["calendar", "contacts", "telemost"],
|
|
169
148
|
},
|
|
170
149
|
];
|
|
171
150
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
@@ -2040,7 +2019,7 @@ async function doctor(args = []) {
|
|
|
2040
2019
|
openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
|
|
2041
2020
|
openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
|
|
2042
2021
|
yandexGeocoderKey: (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) ? "env" : secrets.yandexGeocoder?.apiKey ? "local" : "missing",
|
|
2043
|
-
yandexConnector: (
|
|
2022
|
+
yandexConnector: getYandexConnectorSecretStatus(secrets),
|
|
2044
2023
|
yandexAuthorized: config.yandex?.authorizedServices?.join(", ") || "-",
|
|
2045
2024
|
yandexServices: config.yandex?.enabledServices?.join(", ") || (secrets.cloud?.["yandex-disk"]?.token ? "disk (legacy cloud token)" : "-"),
|
|
2046
2025
|
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
|
|
@@ -3285,7 +3264,7 @@ async function handleYandex(args) {
|
|
|
3285
3264
|
iola yandex disable mail
|
|
3286
3265
|
iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
|
|
3287
3266
|
iola yandex token set
|
|
3288
|
-
iola yandex token delete
|
|
3267
|
+
iola yandex token delete удалить локальные токены и настройки коннектора
|
|
3289
3268
|
iola yandex backlog`);
|
|
3290
3269
|
}
|
|
3291
3270
|
|
|
@@ -3343,11 +3322,12 @@ async function setupYandexConnector(args = []) {
|
|
|
3343
3322
|
}
|
|
3344
3323
|
await printYandexConnectorStatus({ check: true });
|
|
3345
3324
|
} else {
|
|
3346
|
-
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3325
|
+
console.log("Откройте ссылки авторизации, получите OAuth-токены и сохраните их командой: iola yandex token set --app APP_ID");
|
|
3326
|
+
for (const app of oauthApps.length ? oauthApps : [{ id: "custom", title: "Yandex Connector", clientId, services: authorizedServices }]) {
|
|
3327
|
+
const url = buildYandexOAuthUrl({ clientId: app.clientId, services: app.services, redirectUrl });
|
|
3328
|
+
console.log(`${app.id}: ${url}`);
|
|
3329
|
+
if (options.open) await openUrl(url);
|
|
3330
|
+
}
|
|
3351
3331
|
}
|
|
3352
3332
|
} else {
|
|
3353
3333
|
console.log("Yandex Connector не может открыть браузер: в этой сборке не задан public OAuth client_id приложения IOLA.");
|
|
@@ -3362,7 +3342,8 @@ async function chooseYandexServicesMenu() {
|
|
|
3362
3342
|
return;
|
|
3363
3343
|
}
|
|
3364
3344
|
const config = await loadConfig();
|
|
3365
|
-
const serviceIds =
|
|
3345
|
+
const serviceIds = getYandexConnectorMenuServiceIds();
|
|
3346
|
+
const deleteNumber = serviceIds.length + 1;
|
|
3366
3347
|
const enabled = new Set(config.yandex?.enabledServices?.length ? config.yandex.enabledServices : ["identity", "disk"]);
|
|
3367
3348
|
const authState = await getYandexServiceAuthState();
|
|
3368
3349
|
console.log("Функции Яндекса.");
|
|
@@ -3374,6 +3355,7 @@ async function chooseYandexServicesMenu() {
|
|
|
3374
3355
|
const authLabel = auth?.hasToken ? "подключено" : (auth?.authorized ? "нужен вход" : "нет прав");
|
|
3375
3356
|
console.log(`${index + 1}. [${marker}] ${service.title} - ${service.hint} (${service.status}, ${authLabel})`);
|
|
3376
3357
|
});
|
|
3358
|
+
console.log(`${deleteNumber}. Удалить подключение-коннектор`);
|
|
3377
3359
|
console.log("0. Отмена");
|
|
3378
3360
|
const defaults = serviceIds.map((id, index) => enabled.has(id) ? String(index + 1) : "").filter(Boolean);
|
|
3379
3361
|
const answer = (await askText(`Номера через запятую [${defaults.join(",") || "1,2"}]: `)).trim();
|
|
@@ -3382,6 +3364,16 @@ async function chooseYandexServicesMenu() {
|
|
|
3382
3364
|
return;
|
|
3383
3365
|
}
|
|
3384
3366
|
const selectedNumbers = answer ? answer.split(/[,\s]+/).filter(Boolean) : (defaults.length ? defaults : ["1", "2"]);
|
|
3367
|
+
if (selectedNumbers.includes(String(deleteNumber))) {
|
|
3368
|
+
if (selectedNumbers.length > 1) throw new Error("Удаление коннектора выбирается отдельно, без других пунктов.");
|
|
3369
|
+
const ok = await askYesNo("Удалить локальные токены и настройки Yandex Connector? [y/N] ", false);
|
|
3370
|
+
if (!ok) {
|
|
3371
|
+
console.log("Удаление отменено.");
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
await deleteYandexConnectorToken();
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3385
3377
|
const selected = selectedNumbers.map((item) => {
|
|
3386
3378
|
const index = Number(item) - 1;
|
|
3387
3379
|
if (!Number.isInteger(index) || index < 0 || index >= serviceIds.length) {
|
|
@@ -3397,6 +3389,12 @@ async function chooseYandexServicesMenu() {
|
|
|
3397
3389
|
if (missingToken.length) console.log(`Нужно пройти вход Яндекса для: ${missingToken.join(", ")}. Запустите iola yandex setup.`);
|
|
3398
3390
|
}
|
|
3399
3391
|
|
|
3392
|
+
function getYandexConnectorMenuServiceIds() {
|
|
3393
|
+
return Object.entries(YANDEX_CONNECTOR_SERVICES)
|
|
3394
|
+
.filter(([, service]) => service.status === "ready" || service.status === "research")
|
|
3395
|
+
.map(([id]) => id);
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3400
3398
|
async function updateYandexEnabledServices(rawServices, enabled) {
|
|
3401
3399
|
const config = await loadConfig();
|
|
3402
3400
|
const current = new Set(config.yandex?.enabledServices || []);
|
|
@@ -3440,11 +3438,15 @@ async function saveYandexAuthorizedServices(services) {
|
|
|
3440
3438
|
async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
|
|
3441
3439
|
const options = parseOptions(rawArgs);
|
|
3442
3440
|
const config = await loadConfig();
|
|
3443
|
-
const
|
|
3444
|
-
if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
|
|
3441
|
+
const apps = getConfiguredYandexOAuthApps();
|
|
3445
3442
|
const capableServices = getYandexOAuthCapableServiceIds();
|
|
3446
3443
|
const requestedServices = normalizeYandexServiceList(options._.length ? options._ : capableServices);
|
|
3447
|
-
const
|
|
3444
|
+
const app = options.app
|
|
3445
|
+
? apps.find((item) => item.id === options.app)
|
|
3446
|
+
: apps.find((item) => requestedServices.some((service) => item.services.includes(service))) || apps[0];
|
|
3447
|
+
const clientId = options["client-id"] || app?.clientId || config.yandex?.oauth?.clientId || YANDEX_CONNECTOR_CLIENT_ID;
|
|
3448
|
+
if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
|
|
3449
|
+
const services = requestedServices.filter((id) => capableServices.includes(id) && (!app || app.services.includes(id)));
|
|
3448
3450
|
return buildYandexOAuthUrl({ clientId, services, redirectUrl: options["redirect-url"] || config.yandex?.oauth?.redirectUrl || YANDEX_OAUTH_REDIRECT_URL });
|
|
3449
3451
|
}
|
|
3450
3452
|
|
|
@@ -3463,7 +3465,16 @@ function getYandexConnectorRedirectUrl() {
|
|
|
3463
3465
|
}
|
|
3464
3466
|
|
|
3465
3467
|
async function runYandexBrowserOAuth({ appId = "core", clientId, services, redirectUrl }) {
|
|
3466
|
-
|
|
3468
|
+
let token = "";
|
|
3469
|
+
try {
|
|
3470
|
+
token = await waitForYandexOAuthToken({ clientId, services, redirectUrl });
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
if (!process.stdin.isTTY || !String(error?.message || error).includes("Время ожидания")) throw error;
|
|
3473
|
+
console.log("Автоматический прием OAuth-токена не сработал.");
|
|
3474
|
+
console.log("Если в адресной строке браузера есть access_token, вставьте только значение access_token.");
|
|
3475
|
+
token = (await askText("Yandex OAuth access_token [Enter - пропустить]: ")).trim();
|
|
3476
|
+
if (!token) throw error;
|
|
3477
|
+
}
|
|
3467
3478
|
await setYandexConnectorToken(["--token", token, "--app", appId]);
|
|
3468
3479
|
console.log("Yandex Connector подключен.");
|
|
3469
3480
|
}
|
|
@@ -3472,10 +3483,28 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
|
|
|
3472
3483
|
return new Promise((resolvePromise, reject) => {
|
|
3473
3484
|
let settled = false;
|
|
3474
3485
|
const timeoutMs = 180000;
|
|
3486
|
+
const finish = (token) => {
|
|
3487
|
+
if (!token) throw new Error("Yandex OAuth token не получен.");
|
|
3488
|
+
if (!settled) {
|
|
3489
|
+
settled = true;
|
|
3490
|
+
clearTimeout(timer);
|
|
3491
|
+
resolvePromise(String(token));
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3475
3494
|
const server = createServer(async (req, res) => {
|
|
3476
3495
|
try {
|
|
3477
3496
|
const url = new URL(req.url || "/", redirectUrl);
|
|
3478
3497
|
if (url.pathname === YANDEX_CONNECTOR_REDIRECT_PATH && req.method === "GET") {
|
|
3498
|
+
const tokenFromQuery = url.searchParams.get("access_token") || url.searchParams.get("token");
|
|
3499
|
+
const errorFromQuery = url.searchParams.get("error") || "";
|
|
3500
|
+
if (errorFromQuery) throw new Error(`Yandex OAuth error: ${errorFromQuery}`);
|
|
3501
|
+
if (tokenFromQuery) {
|
|
3502
|
+
finish(tokenFromQuery);
|
|
3503
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3504
|
+
res.end("<p>Yandex Connector подключен. Можно закрыть вкладку и вернуться в терминал.</p>");
|
|
3505
|
+
server.close();
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3479
3508
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3480
3509
|
res.end(`<!doctype html>
|
|
3481
3510
|
<html lang="ru"><head><meta charset="utf-8"><title>IOLA Yandex Connector</title></head>
|
|
@@ -3483,33 +3512,45 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
|
|
|
3483
3512
|
<p>Передаю токен в iola-cli...</p>
|
|
3484
3513
|
<script>
|
|
3485
3514
|
(async () => {
|
|
3486
|
-
const params = new URLSearchParams(location.hash.replace(/^#/, ""));
|
|
3515
|
+
const params = new URLSearchParams(location.hash.replace(/^#/, "") || location.search.replace(/^\\?/, ""));
|
|
3487
3516
|
const token = params.get("access_token");
|
|
3488
3517
|
const error = params.get("error") || "";
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3518
|
+
const qs = new URLSearchParams({ token: token || "", error }).toString();
|
|
3519
|
+
try {
|
|
3520
|
+
await fetch("/yandex/oauth/token", {
|
|
3521
|
+
method: "POST",
|
|
3522
|
+
headers: { "content-type": "application/json" },
|
|
3523
|
+
body: JSON.stringify({ token, error })
|
|
3524
|
+
});
|
|
3525
|
+
} catch {
|
|
3526
|
+
location.replace("/yandex/oauth/token?" + qs);
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3494
3529
|
document.body.innerHTML = token
|
|
3495
3530
|
? "<p>Yandex Connector подключен. Можно закрыть вкладку и вернуться в терминал.</p>"
|
|
3496
3531
|
: "<p>Не удалось получить токен. Вернитесь в терминал.</p>";
|
|
3532
|
+
if (!token || error) location.replace("/yandex/oauth/token?" + qs);
|
|
3497
3533
|
})();
|
|
3498
3534
|
</script>
|
|
3499
3535
|
</body></html>`);
|
|
3500
3536
|
return;
|
|
3501
3537
|
}
|
|
3538
|
+
if (url.pathname === "/yandex/oauth/token" && req.method === "GET") {
|
|
3539
|
+
const token = url.searchParams.get("token") || url.searchParams.get("access_token");
|
|
3540
|
+
const error = url.searchParams.get("error") || "";
|
|
3541
|
+
if (error) throw new Error(`Yandex OAuth error: ${error}`);
|
|
3542
|
+
finish(token);
|
|
3543
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3544
|
+
res.end("<p>Yandex Connector подключен. Можно закрыть вкладку и вернуться в терминал.</p>");
|
|
3545
|
+
server.close();
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3502
3548
|
if (url.pathname === "/yandex/oauth/token" && req.method === "POST") {
|
|
3503
3549
|
const chunks = [];
|
|
3504
3550
|
for await (const chunk of req) chunks.push(chunk);
|
|
3505
3551
|
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
3506
3552
|
if (payload.error) throw new Error(`Yandex OAuth error: ${payload.error}`);
|
|
3507
|
-
|
|
3508
|
-
if (!settled) {
|
|
3509
|
-
settled = true;
|
|
3510
|
-
clearTimeout(timer);
|
|
3511
|
-
resolvePromise(String(payload.token));
|
|
3512
|
-
}
|
|
3553
|
+
finish(payload.token);
|
|
3513
3554
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3514
3555
|
res.end(JSON.stringify({ ok: true }));
|
|
3515
3556
|
server.close();
|
|
@@ -3557,7 +3598,6 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
|
|
|
3557
3598
|
function getYandexScopesForServices(services) {
|
|
3558
3599
|
const scopes = new Set();
|
|
3559
3600
|
const normalized = normalizeYandexServiceList(services);
|
|
3560
|
-
if (normalized.length > 0 && !normalized.includes("identity")) normalized.unshift("identity");
|
|
3561
3601
|
for (const id of normalized) {
|
|
3562
3602
|
const raw = YANDEX_CONNECTOR_SERVICES[id]?.scope || "";
|
|
3563
3603
|
for (const scope of raw.split(/\s+/).filter(Boolean)) scopes.add(scope);
|
|
@@ -3581,6 +3621,30 @@ function getYandexOAuthAppById(appId) {
|
|
|
3581
3621
|
|| null;
|
|
3582
3622
|
}
|
|
3583
3623
|
|
|
3624
|
+
function getYandexConnectorConnectedAppIds(secrets = {}) {
|
|
3625
|
+
const apps = new Set(Object.keys(secrets.yandex?.oauthApps || {}));
|
|
3626
|
+
if (secrets.yandex?.oauthToken) apps.add("core");
|
|
3627
|
+
if (secrets.cloud?.["yandex-disk"]?.token) apps.add("core");
|
|
3628
|
+
return apps;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
function isYandexConnectorFullyConnected(secrets = {}) {
|
|
3632
|
+
if (process.env.YANDEX_OAUTH_TOKEN) return true;
|
|
3633
|
+
const connected = getYandexConnectorConnectedAppIds(secrets);
|
|
3634
|
+
const required = getConfiguredYandexOAuthApps().map((app) => app.id);
|
|
3635
|
+
return required.length > 0 && required.every((id) => connected.has(id));
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
function getYandexConnectorSecretStatus(secrets = {}) {
|
|
3639
|
+
if (process.env.YANDEX_OAUTH_TOKEN) return "env";
|
|
3640
|
+
const connected = getYandexConnectorConnectedAppIds(secrets);
|
|
3641
|
+
const required = getConfiguredYandexOAuthApps().map((app) => app.id);
|
|
3642
|
+
if (required.length === 0) return connected.size ? "local" : "missing";
|
|
3643
|
+
if (required.every((id) => connected.has(id))) return "ready";
|
|
3644
|
+
if (connected.size > 0) return `partial (${[...connected].join(", ")})`;
|
|
3645
|
+
return "missing";
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3584
3648
|
async function setYandexConnectorToken(args = []) {
|
|
3585
3649
|
const options = parseOptions(args);
|
|
3586
3650
|
const appId = options.app || "core";
|
|
@@ -3614,7 +3678,18 @@ async function deleteYandexConnectorToken() {
|
|
|
3614
3678
|
if (secrets.cloud?.["yandex-disk"]) delete secrets.cloud["yandex-disk"];
|
|
3615
3679
|
if (secrets.cloud && Object.keys(secrets.cloud).length === 0) delete secrets.cloud;
|
|
3616
3680
|
await saveSecrets(secrets);
|
|
3617
|
-
|
|
3681
|
+
await deleteLocalYandexConnectorConfig();
|
|
3682
|
+
console.log("Yandex Connector удален локально. Токены и настройки приложений очищены.");
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
async function deleteLocalYandexConnectorConfig() {
|
|
3686
|
+
const local = await readConfigLayer(CONFIG_FILE);
|
|
3687
|
+
if (!local) return;
|
|
3688
|
+
delete local.yandex;
|
|
3689
|
+
if (local.cloud?.activeProvider === "yandex-disk") local.cloud.activeProvider = "";
|
|
3690
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
3691
|
+
if (existsSync(CONFIG_FILE)) await copyFile(CONFIG_FILE, LAST_GOOD_CONFIG_FILE).catch(() => {});
|
|
3692
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(local, null, 2)}\n`, "utf8");
|
|
3618
3693
|
}
|
|
3619
3694
|
|
|
3620
3695
|
async function printYandexConnectorStatus(options = {}) {
|
|
@@ -11390,7 +11465,7 @@ async function getOnboardComponentStatus() {
|
|
|
11390
11465
|
browser: browser.installed === "yes",
|
|
11391
11466
|
"yandex-geocoder": Boolean(yandexGeocoderKey),
|
|
11392
11467
|
cloud: Object.keys(cloudSecrets).length > 0,
|
|
11393
|
-
yandex:
|
|
11468
|
+
yandex: isYandexConnectorFullyConnected(secrets),
|
|
11394
11469
|
};
|
|
11395
11470
|
}
|
|
11396
11471
|
|
|
@@ -13415,6 +13490,12 @@ function sanitizeConfig(config) {
|
|
|
13415
13490
|
if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("local-files") && !next.skills.enabled.includes("personal-docs")) {
|
|
13416
13491
|
next.skills.enabled = [...next.skills.enabled, "personal-docs"];
|
|
13417
13492
|
}
|
|
13493
|
+
if (Array.isArray(next.yandex?.enabledServices)) {
|
|
13494
|
+
next.yandex.enabledServices = next.yandex.enabledServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
13495
|
+
}
|
|
13496
|
+
if (Array.isArray(next.yandex?.authorizedServices)) {
|
|
13497
|
+
next.yandex.authorizedServices = next.yandex.authorizedServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
13498
|
+
}
|
|
13418
13499
|
const localProfile = next.ai?.profiles?.local;
|
|
13419
13500
|
if (localProfile?.provider === "iola") {
|
|
13420
13501
|
if (!localProfile.runtime || localProfile.model === "iola-router-1b") {
|
package/test/smoke-test.js
CHANGED
|
@@ -60,11 +60,17 @@ assertIncludes(cliSource, "Другая Ollama-модель", "Local model selec
|
|
|
60
60
|
assertIncludes(cliSource, "chooseYandexServicesMenu", "Yandex Connector should have a service selection menu");
|
|
61
61
|
assertIncludes(cliSource, "Функции Яндекса.", "Yandex service selection should use a numbered menu");
|
|
62
62
|
assertIncludes(cliSource, "Выберите номера функций через запятую", "Yandex service selection should ask for numbers");
|
|
63
|
+
assertIncludes(cliSource, "Удалить подключение-коннектор", "Yandex service selection should allow connector deletion");
|
|
63
64
|
assertIncludes(cliSource, "getYandexServiceAuthState", "Yandex status should derive permissions from configured OAuth apps");
|
|
64
65
|
assertIncludes(cliSource, "OAuth-права встроенного приложения", "Yandex setup should report packaged OAuth app permissions");
|
|
65
66
|
assertIncludes(cliSource, "Выбрать активные функции можно командой /yandex", "Yandex setup should direct service selection to /yandex");
|
|
66
67
|
assertIncludes(cliSource, "runYandexBrowserOAuth", "Yandex setup should support browser OAuth flow");
|
|
67
68
|
assertIncludes(cliSource, "IOLA_YANDEX_OAUTH_CLIENT_ID", "Yandex setup should use a packaged/env OAuth client id");
|
|
69
|
+
assertIncludes(cliSource, "IOLA_YANDEX_ORGANIZER_OAUTH_CLIENT_ID", "Yandex setup should support the organizer OAuth app group");
|
|
70
|
+
assertIncludes(cliSource, "addressbook:all", "Yandex contacts should use the addressbook OAuth scope");
|
|
71
|
+
assertIncludes(cliSource, "Автоматический прием OAuth-токена не сработал", "Yandex OAuth should provide manual token fallback");
|
|
72
|
+
assertIncludes(cliSource, "partial (", "Yandex connector status should report partial connections");
|
|
73
|
+
assertIncludes(cliSource, "isYandexConnectorFullyConnected", "Yandex master status should require all OAuth app tokens");
|
|
68
74
|
assertIncludes(cliSource, "--app", "Yandex token command should persist tokens by OAuth app group");
|
|
69
75
|
assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
|
|
70
76
|
if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
|
package/wiki/Yandex-Connector.md
CHANGED
|
@@ -21,6 +21,15 @@ iola yandex token set
|
|
|
21
21
|
iola yandex token delete
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## OAuth-приложения
|
|
25
|
+
|
|
26
|
+
Для пользователя это один `Yandex Connector`, но внутри CLI проходит две авторизации Яндекса:
|
|
27
|
+
|
|
28
|
+
- `IOLA CLI A` - Yandex ID, Яндекс Диск, Яндекс Почта, Яндекс Документы через Диск;
|
|
29
|
+
- `IOLA CLI B` - Яндекс Календарь, Яндекс Контакты, Телемост через календарное событие.
|
|
30
|
+
|
|
31
|
+
`client_id` этих приложений публичный и встроен в CLI. Пользователь не создает OAuth-приложения вручную: он только входит в свой Яндекс-аккаунт и разрешает доступ. Токены сохраняются локально на компьютере пользователя.
|
|
32
|
+
|
|
24
33
|
## Категории
|
|
25
34
|
|
|
26
35
|
Готово к первому контуру:
|
|
@@ -33,10 +42,8 @@ iola yandex token delete
|
|
|
33
42
|
- `mail` - Яндекс Почта, чтение и поиск писем, отправка только после явного подтверждения;
|
|
34
43
|
- `calendar` - Яндекс Календарь;
|
|
35
44
|
- `contacts` - Яндекс Контакты;
|
|
36
|
-
- `
|
|
37
|
-
- `
|
|
38
|
-
- `forms` - Yandex Forms;
|
|
39
|
-
- `docs` - Яндекс Документы / 360.
|
|
45
|
+
- `docs` - Яндекс Документы / 360 через Диск;
|
|
46
|
+
- `telemost` - Яндекс Телемост через календарь, если сценарий подтвердится.
|
|
40
47
|
|
|
41
48
|
Отдельные ключи, не обычный OAuth бытового Яндекса:
|
|
42
49
|
|
|
@@ -57,7 +64,7 @@ Backlog после первого контура:
|
|
|
57
64
|
iola yandex setup
|
|
58
65
|
```
|
|
59
66
|
|
|
60
|
-
2. Авторизуйтесь в Яндексе и разрешите
|
|
67
|
+
2. Авторизуйтесь в Яндексе и разрешите доступ для `IOLA CLI A`. Запрашиваются права:
|
|
61
68
|
- `login:info`;
|
|
62
69
|
- `login:email`;
|
|
63
70
|
- `cloud_api:disk.read`;
|
|
@@ -65,15 +72,19 @@ iola yandex setup
|
|
|
65
72
|
- `cloud_api:disk.info`;
|
|
66
73
|
- `mail:imap_full`;
|
|
67
74
|
- `mail:smtp`.
|
|
68
|
-
3.
|
|
69
|
-
|
|
75
|
+
3. Затем CLI откроет вторую авторизацию для `IOLA CLI B`. Запрашиваются права:
|
|
76
|
+
- `calendar:all`;
|
|
77
|
+
- `addressbook:all`.
|
|
78
|
+
4. После каждой успешной авторизации браузер вернется на локальную страницу `iola-cli`, а CLI сам сохранит OAuth-токен нужной группы.
|
|
79
|
+
Если браузер не смог передать токен обратно в CLI автоматически, CLI попросит вставить `access_token` вручную. Вставлять нужно только значение параметра `access_token`, без всего URL.
|
|
80
|
+
5. Если автоматический браузерный flow недоступен, используйте fallback для разработки:
|
|
70
81
|
|
|
71
82
|
```bash
|
|
72
83
|
iola yandex setup --client-id CLIENT_ID --print-url
|
|
73
84
|
iola yandex token set
|
|
74
85
|
```
|
|
75
86
|
|
|
76
|
-
|
|
87
|
+
6. Выберите, какие функции CLI реально использует:
|
|
77
88
|
|
|
78
89
|
```bash
|
|
79
90
|
iola yandex menu
|
|
@@ -86,8 +97,11 @@ iola yandex menu
|
|
|
86
97
|
```
|
|
87
98
|
|
|
88
99
|
Меню `/yandex` работает как мастер настройки: сервисы выбираются номерами через запятую, а не вводом технических названий.
|
|
100
|
+
В этом же меню есть отдельный пункт `Удалить подключение-коннектор`: он удаляет локальные токены и настройки Yandex Connector, чтобы можно было подключить другой Яндекс-аккаунт или отказаться от сервисов Яндекса.
|
|
101
|
+
|
|
102
|
+
В мастере настройки Yandex Connector считается готовым только после сохранения токенов обеих групп: `IOLA CLI A` и `IOLA CLI B`.
|
|
89
103
|
|
|
90
|
-
|
|
104
|
+
7. Проверьте:
|
|
91
105
|
|
|
92
106
|
```bash
|
|
93
107
|
iola yandex doctor
|
|
@@ -103,8 +117,10 @@ OAuth-права дают CLI разрешение обращаться к се
|
|
|
103
117
|
- `identity` - проверить пользователя и email;
|
|
104
118
|
- `disk` - папки, загрузка, скачивание, поиск файлов, публичные ссылки;
|
|
105
119
|
- `mail` - список писем, поиск, чтение письма, отправка письма после явного подтверждения;
|
|
106
|
-
- `calendar` - список событий, создание события, напоминания;
|
|
107
|
-
- `contacts` - поиск контактов и карточки контактов;
|
|
120
|
+
- `calendar` - список событий, создание события, напоминания;
|
|
121
|
+
- `contacts` - поиск контактов и карточки контактов;
|
|
122
|
+
- `docs` - поиск и работа с документами через Диск;
|
|
123
|
+
- `telemost` - подготовка встречи через календарь, если подтвердится.
|
|
108
124
|
|
|
109
125
|
Включение сервиса в `/yandex` только разрешает CLI использовать соответствующую категорию. Если прав или тула еще нет, CLI должен честно показать это, а не имитировать работу.
|
|
110
126
|
|
|
@@ -132,6 +148,6 @@ Yandex Connector не является универсальным ключом
|
|
|
132
148
|
|
|
133
149
|
Для браузерного подключения в сборке CLI уже указан public OAuth `client_id` приложения IOLA. Это не секретный ключ. Пользователь не должен создавать OAuth-приложение вручную.
|
|
134
150
|
|
|
135
|
-
Яндекс ограничивает количество разных групп сервисов в одном OAuth-приложении. Поэтому один токен не
|
|
151
|
+
Яндекс ограничивает количество разных групп сервисов в одном OAuth-приложении. Поэтому один токен не может покрыть Диск, Почту, Календарь и Контакты сразу. CLI использует две встроенные группы: `IOLA CLI A` и `IOLA CLI B`.
|
|
136
152
|
|
|
137
153
|
CLI не должен автоматически оформлять покупки, вызывать такси, подтверждать доставку или выполнять платежи. Для таких сценариев допустима только подготовка ссылки, маршрута или списка, а финальное действие делает пользователь в приложении Яндекса.
|