@iola_adm/iola-cli 0.2.15 → 0.2.17
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 +3 -1
- package/package.json +1 -1
- package/src/cli.js +108 -58
- package/test/smoke-test.js +6 -0
- package/wiki/Yandex-Connector.md +37 -10
package/README.md
CHANGED
|
@@ -197,7 +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
|
+
|
|
202
|
+
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. OAuth-права сами по себе не создают функциональность: под каждый сервис нужны отдельные команды и тулы.
|
|
201
203
|
|
|
202
204
|
Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
|
|
203
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: (process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token) ? "local/env" : "missing",
|
|
2022
|
+
yandexConnector: (process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || Object.keys(secrets.yandex?.oauthApps || {}).length || secrets.cloud?.["yandex-disk"]?.token) ? "local/env" : "missing",
|
|
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,14 +3342,20 @@ 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();
|
|
3349
|
+
console.log("Функции Яндекса.");
|
|
3350
|
+
console.log("Выберите номера функций через запятую:");
|
|
3368
3351
|
serviceIds.forEach((id, index) => {
|
|
3369
3352
|
const service = YANDEX_CONNECTOR_SERVICES[id];
|
|
3370
3353
|
const marker = enabled.has(id) ? "✓" : " ";
|
|
3371
|
-
|
|
3354
|
+
const auth = authState.byService[id];
|
|
3355
|
+
const authLabel = auth?.hasToken ? "подключено" : (auth?.authorized ? "нужен вход" : "нет прав");
|
|
3356
|
+
console.log(`${index + 1}. [${marker}] ${service.title} - ${service.hint} (${service.status}, ${authLabel})`);
|
|
3372
3357
|
});
|
|
3358
|
+
console.log(`${deleteNumber}. Удалить подключение-коннектор`);
|
|
3373
3359
|
console.log("0. Отмена");
|
|
3374
3360
|
const defaults = serviceIds.map((id, index) => enabled.has(id) ? String(index + 1) : "").filter(Boolean);
|
|
3375
3361
|
const answer = (await askText(`Номера через запятую [${defaults.join(",") || "1,2"}]: `)).trim();
|
|
@@ -3378,6 +3364,16 @@ async function chooseYandexServicesMenu() {
|
|
|
3378
3364
|
return;
|
|
3379
3365
|
}
|
|
3380
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
|
+
}
|
|
3381
3377
|
const selected = selectedNumbers.map((item) => {
|
|
3382
3378
|
const index = Number(item) - 1;
|
|
3383
3379
|
if (!Number.isInteger(index) || index < 0 || index >= serviceIds.length) {
|
|
@@ -3387,13 +3383,16 @@ async function chooseYandexServicesMenu() {
|
|
|
3387
3383
|
});
|
|
3388
3384
|
await saveYandexEnabledServices(selected);
|
|
3389
3385
|
console.log(`Включены сервисы: ${normalizeYandexServiceList(selected).join(", ")}`);
|
|
3390
|
-
const
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3386
|
+
const missingAuth = selected.filter((id) => !authState.byService[id]?.authorized);
|
|
3387
|
+
const missingToken = selected.filter((id) => authState.byService[id]?.authorized && !authState.byService[id]?.hasToken);
|
|
3388
|
+
if (missingAuth.length) console.log(`Нет OAuth-прав в текущей сборке: ${missingAuth.join(", ")}. Для них нужно отдельное OAuth-приложение Яндекса.`);
|
|
3389
|
+
if (missingToken.length) console.log(`Нужно пройти вход Яндекса для: ${missingToken.join(", ")}. Запустите iola yandex setup.`);
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
function getYandexConnectorMenuServiceIds() {
|
|
3393
|
+
return Object.entries(YANDEX_CONNECTOR_SERVICES)
|
|
3394
|
+
.filter(([, service]) => service.status === "ready" || service.status === "research")
|
|
3395
|
+
.map(([id]) => id);
|
|
3397
3396
|
}
|
|
3398
3397
|
|
|
3399
3398
|
async function updateYandexEnabledServices(rawServices, enabled) {
|
|
@@ -3439,9 +3438,15 @@ async function saveYandexAuthorizedServices(services) {
|
|
|
3439
3438
|
async function buildYandexOAuthUrlFromConfig(rawArgs = []) {
|
|
3440
3439
|
const options = parseOptions(rawArgs);
|
|
3441
3440
|
const config = await loadConfig();
|
|
3442
|
-
const
|
|
3441
|
+
const apps = getConfiguredYandexOAuthApps();
|
|
3442
|
+
const capableServices = getYandexOAuthCapableServiceIds();
|
|
3443
|
+
const requestedServices = normalizeYandexServiceList(options._.length ? options._ : capableServices);
|
|
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;
|
|
3443
3448
|
if (!clientId) throw new Error("Yandex OAuth Client ID не задан. Пример: iola yandex oauth-url disk --client-id CLIENT_ID");
|
|
3444
|
-
const services =
|
|
3449
|
+
const services = requestedServices.filter((id) => capableServices.includes(id) && (!app || app.services.includes(id)));
|
|
3445
3450
|
return buildYandexOAuthUrl({ clientId, services, redirectUrl: options["redirect-url"] || config.yandex?.oauth?.redirectUrl || YANDEX_OAUTH_REDIRECT_URL });
|
|
3446
3451
|
}
|
|
3447
3452
|
|
|
@@ -3554,7 +3559,6 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
|
|
|
3554
3559
|
function getYandexScopesForServices(services) {
|
|
3555
3560
|
const scopes = new Set();
|
|
3556
3561
|
const normalized = normalizeYandexServiceList(services);
|
|
3557
|
-
if (normalized.length > 0 && !normalized.includes("identity")) normalized.unshift("identity");
|
|
3558
3562
|
for (const id of normalized) {
|
|
3559
3563
|
const raw = YANDEX_CONNECTOR_SERVICES[id]?.scope || "";
|
|
3560
3564
|
for (const scope of raw.split(/\s+/).filter(Boolean)) scopes.add(scope);
|
|
@@ -3611,22 +3615,33 @@ async function deleteYandexConnectorToken() {
|
|
|
3611
3615
|
if (secrets.cloud?.["yandex-disk"]) delete secrets.cloud["yandex-disk"];
|
|
3612
3616
|
if (secrets.cloud && Object.keys(secrets.cloud).length === 0) delete secrets.cloud;
|
|
3613
3617
|
await saveSecrets(secrets);
|
|
3614
|
-
|
|
3618
|
+
await deleteLocalYandexConnectorConfig();
|
|
3619
|
+
console.log("Yandex Connector удален локально. Токены и настройки приложений очищены.");
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
async function deleteLocalYandexConnectorConfig() {
|
|
3623
|
+
const local = await readConfigLayer(CONFIG_FILE);
|
|
3624
|
+
if (!local) return;
|
|
3625
|
+
delete local.yandex;
|
|
3626
|
+
if (local.cloud?.activeProvider === "yandex-disk") local.cloud.activeProvider = "";
|
|
3627
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
3628
|
+
if (existsSync(CONFIG_FILE)) await copyFile(CONFIG_FILE, LAST_GOOD_CONFIG_FILE).catch(() => {});
|
|
3629
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(local, null, 2)}\n`, "utf8");
|
|
3615
3630
|
}
|
|
3616
3631
|
|
|
3617
3632
|
async function printYandexConnectorStatus(options = {}) {
|
|
3618
3633
|
const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
|
|
3619
3634
|
const enabled = config.yandex?.enabledServices || [];
|
|
3620
|
-
const authorized = config.yandex?.authorizedServices?.length ? config.yandex.authorizedServices : [];
|
|
3621
3635
|
const legacyDiskToken = Boolean(secrets.cloud?.["yandex-disk"]?.token && !secrets.yandex?.oauthToken);
|
|
3636
|
+
const authState = await getYandexServiceAuthState({ config, secrets });
|
|
3622
3637
|
const token = process.env.YANDEX_OAUTH_TOKEN || secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token || "";
|
|
3623
3638
|
const rows = Object.entries(YANDEX_CONNECTOR_SERVICES).map(([id, service]) => ({
|
|
3624
3639
|
id,
|
|
3625
3640
|
enabled: enabled.includes(id) ? "yes" : (legacyDiskToken && id === "disk" ? "legacy" : "no"),
|
|
3626
3641
|
category: service.category,
|
|
3627
3642
|
status: service.status,
|
|
3628
|
-
token: service.scope &&
|
|
3629
|
-
authorized:
|
|
3643
|
+
token: service.scope && authState.byService[id]?.authorized ? (authState.byService[id]?.hasToken ? "local/env" : "missing") : "-",
|
|
3644
|
+
authorized: authState.byService[id]?.authorized ? "yes" : "-",
|
|
3630
3645
|
title: service.title,
|
|
3631
3646
|
}));
|
|
3632
3647
|
printTable(rows, [
|
|
@@ -3650,6 +3665,35 @@ async function printYandexConnectorStatus(options = {}) {
|
|
|
3650
3665
|
}
|
|
3651
3666
|
}
|
|
3652
3667
|
|
|
3668
|
+
async function getYandexServiceAuthState({ config = null, secrets = null } = {}) {
|
|
3669
|
+
const loadedConfig = config || await loadConfig();
|
|
3670
|
+
const loadedSecrets = secrets || await loadSecrets();
|
|
3671
|
+
const apps = getConfiguredYandexOAuthApps();
|
|
3672
|
+
const appTokens = loadedSecrets.yandex?.oauthApps || {};
|
|
3673
|
+
const envToken = process.env.YANDEX_OAUTH_TOKEN || "";
|
|
3674
|
+
const byService = {};
|
|
3675
|
+
for (const id of Object.keys(YANDEX_CONNECTOR_SERVICES)) {
|
|
3676
|
+
byService[id] = { authorized: false, hasToken: false, apps: [] };
|
|
3677
|
+
}
|
|
3678
|
+
for (const app of apps) {
|
|
3679
|
+
const hasToken = Boolean(envToken || appTokens[app.id]?.token || (app.id === "core" && loadedSecrets.yandex?.oauthToken));
|
|
3680
|
+
for (const id of normalizeYandexServiceList(app.services)) {
|
|
3681
|
+
byService[id] = byService[id] || { authorized: false, hasToken: false, apps: [] };
|
|
3682
|
+
byService[id].authorized = true;
|
|
3683
|
+
byService[id].hasToken = byService[id].hasToken || hasToken;
|
|
3684
|
+
byService[id].apps.push(app.id);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
if (loadedSecrets.cloud?.["yandex-disk"]?.token) {
|
|
3688
|
+
byService.disk.authorized = true;
|
|
3689
|
+
byService.disk.hasToken = true;
|
|
3690
|
+
byService.disk.apps.push("legacy-cloud");
|
|
3691
|
+
}
|
|
3692
|
+
const authorizedServices = Object.entries(byService).filter(([, state]) => state.authorized).map(([id]) => id);
|
|
3693
|
+
const enabledServices = loadedConfig.yandex?.enabledServices || [];
|
|
3694
|
+
return { byService, authorizedServices, enabledServices };
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3653
3697
|
async function yandexUserInfo(token) {
|
|
3654
3698
|
const response = await fetch("https://login.yandex.ru/info?format=json", {
|
|
3655
3699
|
headers: { Authorization: `OAuth ${token}` },
|
|
@@ -11358,7 +11402,7 @@ async function getOnboardComponentStatus() {
|
|
|
11358
11402
|
browser: browser.installed === "yes",
|
|
11359
11403
|
"yandex-geocoder": Boolean(yandexGeocoderKey),
|
|
11360
11404
|
cloud: Object.keys(cloudSecrets).length > 0,
|
|
11361
|
-
yandex: Boolean(secrets.yandex?.oauthToken || config.yandex?.enabledServices?.length),
|
|
11405
|
+
yandex: Boolean(secrets.yandex?.oauthToken || Object.keys(secrets.yandex?.oauthApps || {}).length || config.yandex?.enabledServices?.length),
|
|
11362
11406
|
};
|
|
11363
11407
|
}
|
|
11364
11408
|
|
|
@@ -13383,6 +13427,12 @@ function sanitizeConfig(config) {
|
|
|
13383
13427
|
if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("local-files") && !next.skills.enabled.includes("personal-docs")) {
|
|
13384
13428
|
next.skills.enabled = [...next.skills.enabled, "personal-docs"];
|
|
13385
13429
|
}
|
|
13430
|
+
if (Array.isArray(next.yandex?.enabledServices)) {
|
|
13431
|
+
next.yandex.enabledServices = next.yandex.enabledServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
13432
|
+
}
|
|
13433
|
+
if (Array.isArray(next.yandex?.authorizedServices)) {
|
|
13434
|
+
next.yandex.authorizedServices = next.yandex.authorizedServices.filter((service) => Boolean(YANDEX_CONNECTOR_SERVICES[service]));
|
|
13435
|
+
}
|
|
13386
13436
|
const localProfile = next.ai?.profiles?.local;
|
|
13387
13437
|
if (localProfile?.provider === "iola") {
|
|
13388
13438
|
if (!localProfile.runtime || localProfile.model === "iola-router-1b") {
|
package/test/smoke-test.js
CHANGED
|
@@ -58,10 +58,16 @@ assertIncludes(cliSource, "dedupeDatedOpenAiModels", "OpenAI model selection sho
|
|
|
58
58
|
assertIncludes(cliSource, "chooseLocalModel", "Local model selection should support IOLA and Ollama models");
|
|
59
59
|
assertIncludes(cliSource, "Другая Ollama-модель", "Local model selection should allow manual Ollama model names");
|
|
60
60
|
assertIncludes(cliSource, "chooseYandexServicesMenu", "Yandex Connector should have a service selection menu");
|
|
61
|
+
assertIncludes(cliSource, "Функции Яндекса.", "Yandex service selection should use a numbered menu");
|
|
62
|
+
assertIncludes(cliSource, "Выберите номера функций через запятую", "Yandex service selection should ask for numbers");
|
|
63
|
+
assertIncludes(cliSource, "Удалить подключение-коннектор", "Yandex service selection should allow connector deletion");
|
|
64
|
+
assertIncludes(cliSource, "getYandexServiceAuthState", "Yandex status should derive permissions from configured OAuth apps");
|
|
61
65
|
assertIncludes(cliSource, "OAuth-права встроенного приложения", "Yandex setup should report packaged OAuth app permissions");
|
|
62
66
|
assertIncludes(cliSource, "Выбрать активные функции можно командой /yandex", "Yandex setup should direct service selection to /yandex");
|
|
63
67
|
assertIncludes(cliSource, "runYandexBrowserOAuth", "Yandex setup should support browser OAuth flow");
|
|
64
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");
|
|
65
71
|
assertIncludes(cliSource, "--app", "Yandex token command should persist tokens by OAuth app group");
|
|
66
72
|
assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
|
|
67
73
|
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,18 @@ 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
|
+
5. Если автоматический браузерный flow недоступен, используйте fallback для разработки:
|
|
70
80
|
|
|
71
81
|
```bash
|
|
72
82
|
iola yandex setup --client-id CLIENT_ID --print-url
|
|
73
83
|
iola yandex token set
|
|
74
84
|
```
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
6. Выберите, какие функции CLI реально использует:
|
|
77
87
|
|
|
78
88
|
```bash
|
|
79
89
|
iola yandex menu
|
|
@@ -85,7 +95,10 @@ iola yandex menu
|
|
|
85
95
|
/yandex
|
|
86
96
|
```
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
Меню `/yandex` работает как мастер настройки: сервисы выбираются номерами через запятую, а не вводом технических названий.
|
|
99
|
+
В этом же меню есть отдельный пункт `Удалить подключение-коннектор`: он удаляет локальные токены и настройки Yandex Connector, чтобы можно было подключить другой Яндекс-аккаунт или отказаться от сервисов Яндекса.
|
|
100
|
+
|
|
101
|
+
7. Проверьте:
|
|
89
102
|
|
|
90
103
|
```bash
|
|
91
104
|
iola yandex doctor
|
|
@@ -94,6 +107,20 @@ iola cloud doctor
|
|
|
94
107
|
|
|
95
108
|
Если включен `disk`, токен автоматически подключается и к старому облачному провайдеру `yandex-disk`, поэтому команды `iola cloud ...` продолжают работать.
|
|
96
109
|
|
|
110
|
+
## Что значит включить сервис
|
|
111
|
+
|
|
112
|
+
OAuth-права дают CLI разрешение обращаться к сервису Яндекса. Но для практической работы под каждый сервис нужны отдельные команды и тулы:
|
|
113
|
+
|
|
114
|
+
- `identity` - проверить пользователя и email;
|
|
115
|
+
- `disk` - папки, загрузка, скачивание, поиск файлов, публичные ссылки;
|
|
116
|
+
- `mail` - список писем, поиск, чтение письма, отправка письма после явного подтверждения;
|
|
117
|
+
- `calendar` - список событий, создание события, напоминания;
|
|
118
|
+
- `contacts` - поиск контактов и карточки контактов;
|
|
119
|
+
- `docs` - поиск и работа с документами через Диск;
|
|
120
|
+
- `telemost` - подготовка встречи через календарь, если подтвердится.
|
|
121
|
+
|
|
122
|
+
Включение сервиса в `/yandex` только разрешает CLI использовать соответствующую категорию. Если прав или тула еще нет, CLI должен честно показать это, а не имитировать работу.
|
|
123
|
+
|
|
97
124
|
## Иконка приложения
|
|
98
125
|
|
|
99
126
|
CLI поставляется с готовой иконкой OAuth-приложения. При установке она копируется в:
|
|
@@ -118,6 +145,6 @@ Yandex Connector не является универсальным ключом
|
|
|
118
145
|
|
|
119
146
|
Для браузерного подключения в сборке CLI уже указан public OAuth `client_id` приложения IOLA. Это не секретный ключ. Пользователь не должен создавать OAuth-приложение вручную.
|
|
120
147
|
|
|
121
|
-
Яндекс ограничивает количество разных групп сервисов в одном OAuth-приложении. Поэтому один токен не
|
|
148
|
+
Яндекс ограничивает количество разных групп сервисов в одном OAuth-приложении. Поэтому один токен не может покрыть Диск, Почту, Календарь и Контакты сразу. CLI использует две встроенные группы: `IOLA CLI A` и `IOLA CLI B`.
|
|
122
149
|
|
|
123
150
|
CLI не должен автоматически оформлять покупки, вызывать такси, подтверждать доставку или выполнять платежи. Для таких сценариев допустима только подготовка ссылки, маршрута или списка, а финальное действие делает пользователь в приложении Яндекса.
|