@iola_adm/iola-cli 0.2.38 → 0.2.40
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/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3575,6 +3575,7 @@ async function handleYandexCloudConnector(args = []) {
|
|
|
3575
3575
|
}
|
|
3576
3576
|
|
|
3577
3577
|
if (action === "open") {
|
|
3578
|
+
printYandexCloudSetupGuide({ includeYandexGpt: true });
|
|
3578
3579
|
await openUrl("https://console.yandex.cloud/");
|
|
3579
3580
|
return;
|
|
3580
3581
|
}
|
|
@@ -3585,9 +3586,13 @@ async function handleYandexCloudConnector(args = []) {
|
|
|
3585
3586
|
async function setupYandexCloudConnector(options = {}) {
|
|
3586
3587
|
console.log("Yandex Cloud Connector: геокодинг и YandexGPT.");
|
|
3587
3588
|
console.log("Геокодер будет включен по умолчанию. YandexGPT можно выбрать в /model после сохранения ключей.");
|
|
3589
|
+
printYandexCloudSetupGuide({ includeYandexGpt: true });
|
|
3588
3590
|
if (process.stdin.isTTY) {
|
|
3589
3591
|
const openConsole = await askYesNo("Открыть Yandex Cloud Console для получения ключей? [Y/n] ", true);
|
|
3590
|
-
if (openConsole)
|
|
3592
|
+
if (openConsole) {
|
|
3593
|
+
console.log("Открыл Yandex Cloud Console. Вернитесь сюда после создания ключей и вставьте их в CLI.");
|
|
3594
|
+
await openUrl("https://console.yandex.cloud/");
|
|
3595
|
+
}
|
|
3591
3596
|
}
|
|
3592
3597
|
|
|
3593
3598
|
const secrets = await loadSecrets();
|
|
@@ -3601,6 +3606,8 @@ async function setupYandexCloudConnector(options = {}) {
|
|
|
3601
3606
|
return;
|
|
3602
3607
|
}
|
|
3603
3608
|
|
|
3609
|
+
console.log("");
|
|
3610
|
+
console.log("Вставьте ключ API Геокодера. Если ключ еще создается, оставьте мастер открытым, получите ключ в браузере и вернитесь сюда.");
|
|
3604
3611
|
const geocoderKey = (await askText(`YANDEX_GEOCODER_API_KEY${currentGeocoder ? " [уже сохранен, Enter - оставить]" : ""}: `)).trim() || currentGeocoder;
|
|
3605
3612
|
if (!geocoderKey) throw new Error("Для Cloud Connector нужен хотя бы Geocoder API key.");
|
|
3606
3613
|
|
|
@@ -3619,6 +3626,22 @@ async function setupYandexCloudConnector(options = {}) {
|
|
|
3619
3626
|
await printYandexCloudConnectorStatus({ check: true });
|
|
3620
3627
|
}
|
|
3621
3628
|
|
|
3629
|
+
function printYandexCloudSetupGuide(options = {}) {
|
|
3630
|
+
console.log("");
|
|
3631
|
+
console.log("Что делать в открывшейся консоли Яндекса:");
|
|
3632
|
+
console.log("1. Войдите в свой Яндекс-аккаунт.");
|
|
3633
|
+
console.log("2. Если Яндекс попросит создать облако или каталог, создайте их. Названия можно оставить простыми: iola-cli и default.");
|
|
3634
|
+
console.log("3. Для геокодера откройте Кабинет разработчика: https://developer.tech.yandex.ru/services/2");
|
|
3635
|
+
console.log("4. Выберите API Геокодера, создайте API key и скопируйте значение ключа.");
|
|
3636
|
+
console.log("5. Вернитесь в это окно CLI и вставьте ключ в поле YANDEX_GEOCODER_API_KEY.");
|
|
3637
|
+
if (options.includeYandexGpt) {
|
|
3638
|
+
console.log("6. Если нужен YandexGPT: в Yandex Cloud откройте каталог, скопируйте ID каталога, затем создайте сервисный аккаунт и API-ключ с правом yc.ai.foundationModels.execute.");
|
|
3639
|
+
console.log("7. После ключа геокодера CLI отдельно спросит, настраивать ли YandexGPT. Можно ответить n и подключить модель позже через /model.");
|
|
3640
|
+
}
|
|
3641
|
+
console.log("Подробная инструкция: https://github.com/adm-iola/iola-cli/wiki/Yandex-Cloud-Connector");
|
|
3642
|
+
console.log("");
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3622
3645
|
async function saveYandexCloudConnectorSecrets({ geocoderApiKey, yandexgptApiKey, folderId }) {
|
|
3623
3646
|
const secrets = await loadSecrets();
|
|
3624
3647
|
secrets.yandexCloud = {
|
|
@@ -3765,6 +3788,10 @@ async function buildYandexGoDeeplinkFromOptions(options = {}) {
|
|
|
3765
3788
|
const from = options.from || options._?.[0] || "";
|
|
3766
3789
|
const to = options.to || options._?.[1] || "";
|
|
3767
3790
|
if (!from || !to) throw new Error('Укажите маршрут: iola yandex go link --from "Медведево, Школьная 15" --to "Медведево, Советская 20"');
|
|
3791
|
+
const ambiguous = [from, to].find((point) => isAmbiguousPersonalYandexGoPoint(point));
|
|
3792
|
+
if (ambiguous) {
|
|
3793
|
+
throw new Error(`Не знаю адрес "${ambiguous}". Укажите полный адрес отправления или сохраните его в настройках позже. Пример: "такси от Йошкар-Ола, улица ..., дом ... до Администрации Йошкар-Олы".`);
|
|
3794
|
+
}
|
|
3768
3795
|
const fromPoint = await resolveYandexGoPoint(from);
|
|
3769
3796
|
const toPoint = await resolveYandexGoPoint(to);
|
|
3770
3797
|
const tariff = normalizeYandexGoTariff(options.tariff || options.class || options.level || "econom");
|
|
@@ -3787,10 +3814,46 @@ async function buildYandexGoDeeplinkFromOptions(options = {}) {
|
|
|
3787
3814
|
async function resolveYandexGoPoint(query) {
|
|
3788
3815
|
const parsed = parseLonLat(query);
|
|
3789
3816
|
if (parsed) return { lon: parsed.lon, lat: parsed.lat, label: `${parsed.lat}, ${parsed.lon}`, address: "" };
|
|
3790
|
-
const
|
|
3817
|
+
const normalizedQuery = normalizeYandexGoGeocoderQuery(query);
|
|
3818
|
+
const point = await callYandexGeocoder(normalizedQuery);
|
|
3791
3819
|
const coords = parseCoordinates(point?.coordinates);
|
|
3792
3820
|
if (!Number.isFinite(coords.lat) || !Number.isFinite(coords.lon)) throw new Error(`Не смог получить координаты: ${query}`);
|
|
3793
|
-
|
|
3821
|
+
if (!isExpectedYandexGoGeocoderResult(normalizedQuery, point)) {
|
|
3822
|
+
throw new Error(`Геокодер вернул неподходящий адрес для "${query}": ${point?.address || point?.name || "-"}. Уточните адрес полностью, например с городом и улицей.`);
|
|
3823
|
+
}
|
|
3824
|
+
return { lon: coords.lon, lat: coords.lat, label: point.name || query, address: point.address || normalizedQuery };
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
function isAmbiguousPersonalYandexGoPoint(query) {
|
|
3828
|
+
const text = String(query || "").trim().toLocaleLowerCase("ru-RU");
|
|
3829
|
+
return /^(?:дом|дома|мой дом|у меня|у меня дома|от меня|здесь|тут|текущее место|моя геопозиция|мое местоположение)$/iu.test(text);
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
function normalizeYandexGoGeocoderQuery(query) {
|
|
3833
|
+
const text = String(query || "").trim();
|
|
3834
|
+
const lower = text.toLocaleLowerCase("ru-RU");
|
|
3835
|
+
if (/администрац/iu.test(lower) && /(йошкар|иошкар|йошк|yoshkar|yoshkar-ola)/iu.test(lower)) {
|
|
3836
|
+
return "Россия, Республика Марий Эл, Йошкар-Ола, Ленинский проспект, 27";
|
|
3837
|
+
}
|
|
3838
|
+
if (/^(?:администрац(?:ия|ии)?|мэрия)$/iu.test(lower)) {
|
|
3839
|
+
return "Россия, Республика Марий Эл, Йошкар-Ола, Ленинский проспект, 27";
|
|
3840
|
+
}
|
|
3841
|
+
if (!/(йошкар|медведево|сем[её]новк|республика\s+марий\s+эл|марий\s+эл)/iu.test(lower)
|
|
3842
|
+
&& /(администрац|мэрия|школ|сад|лицей|гимназ|ул\.|улица|проспект|пр-т|бульвар|переулок|дом|д\.|\d)/iu.test(lower)) {
|
|
3843
|
+
return `Россия, Республика Марий Эл, Йошкар-Ола, ${text}`;
|
|
3844
|
+
}
|
|
3845
|
+
return text;
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
function isExpectedYandexGoGeocoderResult(query, point) {
|
|
3849
|
+
const expected = String(query || "").toLocaleLowerCase("ru-RU");
|
|
3850
|
+
const actual = `${point?.address || ""} ${point?.name || ""}`.toLocaleLowerCase("ru-RU");
|
|
3851
|
+
if (/(йошкар|ленинский проспект,\s*27|ленинский проспект,\s*дом\s*27)/iu.test(expected)) {
|
|
3852
|
+
return /йошкар-ола|йошкар ола|ленинский проспект,\s*27|ленинский проспект,\s*дом\s*27/iu.test(actual);
|
|
3853
|
+
}
|
|
3854
|
+
if (/медведево/iu.test(expected)) return /медведево/iu.test(actual);
|
|
3855
|
+
if (/сем[её]новк/iu.test(expected)) return /сем[её]новк/iu.test(actual);
|
|
3856
|
+
return true;
|
|
3794
3857
|
}
|
|
3795
3858
|
|
|
3796
3859
|
function parseLonLat(value) {
|
|
@@ -4886,7 +4949,7 @@ async function yandexMailList(options = {}) {
|
|
|
4886
4949
|
const search = await imapCommand(session, `UID SEARCH ${criterion}`);
|
|
4887
4950
|
const uids = parseImapSearchUids(search).slice(-Number(options.limit || 10));
|
|
4888
4951
|
if (!uids.length) return [];
|
|
4889
|
-
const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] BODY.PEEK[TEXT]<0.
|
|
4952
|
+
const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] BODY.PEEK[TEXT]<0.3000>)`, { timeout: 45000 });
|
|
4890
4953
|
return parseImapFetchSummaries(fetch).sort((left, right) => Number(right.uid || 0) - Number(left.uid || 0));
|
|
4891
4954
|
} finally {
|
|
4892
4955
|
await imapClose(session);
|
|
@@ -5615,7 +5678,7 @@ function parseImapFetchSummaries(text, options = {}) {
|
|
|
5615
5678
|
const subject = headers.subject || "";
|
|
5616
5679
|
const from = headers.from || "";
|
|
5617
5680
|
const date = headers.date || "";
|
|
5618
|
-
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0,
|
|
5681
|
+
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 3000);
|
|
5619
5682
|
rows.push({
|
|
5620
5683
|
uid,
|
|
5621
5684
|
date,
|
|
@@ -5746,12 +5809,55 @@ function decodeEmbeddedBase64MailBody(value) {
|
|
|
5746
5809
|
for (const match of matches) {
|
|
5747
5810
|
const clean = match.replace(/\s+/g, "");
|
|
5748
5811
|
if (!/^[A-Z0-9+/]+={0,2}$/iu.test(clean) || clean.length < 160) continue;
|
|
5749
|
-
const candidate =
|
|
5812
|
+
const candidate = decodeBase64Utf8Candidate(clean);
|
|
5750
5813
|
if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(candidate)) return candidate;
|
|
5751
5814
|
}
|
|
5752
5815
|
return text;
|
|
5753
5816
|
}
|
|
5754
5817
|
|
|
5818
|
+
function decodeBase64Utf8Candidate(value) {
|
|
5819
|
+
const clean = String(value || "").replace(/\s+/g, "");
|
|
5820
|
+
const padded = clean + "=".repeat((4 - (clean.length % 4)) % 4);
|
|
5821
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5824
|
+
function looksLikeEncodedMailText(value) {
|
|
5825
|
+
const text = String(value || "").trim();
|
|
5826
|
+
if (!text) return false;
|
|
5827
|
+
const compact = text.replace(/\s+/g, "");
|
|
5828
|
+
if (compact.length < 80) return false;
|
|
5829
|
+
if (/^[A-Z0-9+/]+={0,2}$/iu.test(compact)) return true;
|
|
5830
|
+
const encodedChars = (text.match(/[A-Z0-9+/=]/giu) || []).length;
|
|
5831
|
+
return encodedChars / Math.max(text.length, 1) > 0.82 && !/[А-Яа-яЁё]{3,}/u.test(text);
|
|
5832
|
+
}
|
|
5833
|
+
|
|
5834
|
+
function cleanupDecodedMailText(value) {
|
|
5835
|
+
return String(value || "")
|
|
5836
|
+
.replace(/<style\b[\s\S]*?<\/style>/giu, " ")
|
|
5837
|
+
.replace(/<script\b[\s\S]*?<\/script>/giu, " ")
|
|
5838
|
+
.replace(/<[^>]+>/g, " ")
|
|
5839
|
+
.replace(/https?:\/\/\S+/giu, " ")
|
|
5840
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]+/gu, " ")
|
|
5841
|
+
.replace(/[^\S\n]+/g, " ")
|
|
5842
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
5843
|
+
.trim();
|
|
5844
|
+
}
|
|
5845
|
+
|
|
5846
|
+
function normalizeMailSnippetForDisplay(value, maxLength = 600) {
|
|
5847
|
+
let text = String(value || "").trim();
|
|
5848
|
+
if (!text) return "";
|
|
5849
|
+
if (looksLikeEncodedMailText(text)) {
|
|
5850
|
+
const compact = text.replace(/\s+/g, "");
|
|
5851
|
+
const decoded = cleanupDecodedMailText(decodeBase64Utf8Candidate(compact));
|
|
5852
|
+
if (/[А-Яа-яЁё]{3,}/u.test(decoded) || /[A-ZА-Яа-яЁё]{3,}\s+[A-ZА-Яа-яЁё]{3,}/u.test(decoded)) {
|
|
5853
|
+
text = decoded;
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
text = cleanupDecodedMailText(decodeEmbeddedBase64MailBody(text));
|
|
5857
|
+
if (looksLikeEncodedMailText(text)) return "";
|
|
5858
|
+
return text.replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5755
5861
|
async function yandexDavRequest(url, token, options = {}) {
|
|
5756
5862
|
const response = await fetch(url, {
|
|
5757
5863
|
method: options.method || "GET",
|
|
@@ -12655,6 +12761,26 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
12655
12761
|
latest[0] ? `Самое свежее: ${formatYandexMailSummary(latest[0])}` : "",
|
|
12656
12762
|
].filter(Boolean).join("\n");
|
|
12657
12763
|
}
|
|
12764
|
+
const recentHours = extractRecentMailHours(question);
|
|
12765
|
+
if (recentHours) {
|
|
12766
|
+
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
12767
|
+
const rows = await yandexMailList({ mailbox, limit: 50, unread: /непрочитан/iu.test(normalized) });
|
|
12768
|
+
const since = Date.now() - recentHours * 60 * 60 * 1000;
|
|
12769
|
+
const recentRows = rows.filter((row) => {
|
|
12770
|
+
const time = Date.parse(row.date || "");
|
|
12771
|
+
return Number.isFinite(time) && time >= since;
|
|
12772
|
+
});
|
|
12773
|
+
if (!recentRows.length) return `За последние ${recentHours} ${formatRussianHours(recentHours)} писем не найдено.`;
|
|
12774
|
+
const detailedRows = [];
|
|
12775
|
+
for (const row of recentRows.slice(0, 10)) {
|
|
12776
|
+
const detailed = await yandexMailRead(row.uid, { mailbox, markSeen: false }).catch(() => null);
|
|
12777
|
+
detailedRows.push(detailed && detailed.status !== "not-found" ? { ...row, ...detailed } : row);
|
|
12778
|
+
}
|
|
12779
|
+
return [
|
|
12780
|
+
`За последние ${recentHours} ${formatRussianHours(recentHours)} нашел ${recentRows.length} ${formatRussianLetters(recentRows.length)}:`,
|
|
12781
|
+
...detailedRows.map((row, index) => formatYandexMailDigestItem(row, index + 1)),
|
|
12782
|
+
].join("\n");
|
|
12783
|
+
}
|
|
12658
12784
|
if (isYandexMailReadRequest(normalized)) {
|
|
12659
12785
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText)
|
|
12660
12786
|
|| await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
|
|
@@ -12663,6 +12789,7 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
12663
12789
|
if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
|
|
12664
12790
|
return formatYandexMailRead(row);
|
|
12665
12791
|
}
|
|
12792
|
+
|
|
12666
12793
|
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
12667
12794
|
const rows = /(найди|поиск)/iu.test(normalized)
|
|
12668
12795
|
? await yandexMailSearch(cleanupYandexQuery(question), { mailbox, limit: 10 })
|
|
@@ -12869,7 +12996,7 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
12869
12996
|
}
|
|
12870
12997
|
} catch (error) {
|
|
12871
12998
|
const message = error instanceof Error ? error.message : String(error);
|
|
12872
|
-
if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT
|
|
12999
|
+
if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT нужны|Не знаю адрес|Геокодер вернул неподходящий адрес)/u.test(message)) return message;
|
|
12873
13000
|
return `Не смог выполнить запрос к сервисам Яндекса: ${message}`;
|
|
12874
13001
|
}
|
|
12875
13002
|
return "";
|
|
@@ -13304,8 +13431,19 @@ function formatYandexMailSummary(row) {
|
|
|
13304
13431
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
13305
13432
|
}
|
|
13306
13433
|
|
|
13434
|
+
function formatYandexMailDigestItem(row, index) {
|
|
13435
|
+
const snippet = normalizeMailSnippetForDisplay(row.snippet, 350);
|
|
13436
|
+
return [
|
|
13437
|
+
`${index}. Письмо #${row.uid}`,
|
|
13438
|
+
` От: ${row.from || "-"}`,
|
|
13439
|
+
` Тема: ${row.subject || "(без темы)"}`,
|
|
13440
|
+
row.date ? ` Дата: ${row.date}` : "",
|
|
13441
|
+
snippet ? ` О чем: ${snippet}` : " О чем: текст письма не распознан, доступна только тема.",
|
|
13442
|
+
].filter(Boolean).join("\n");
|
|
13443
|
+
}
|
|
13444
|
+
|
|
13307
13445
|
function formatYandexMailRead(row) {
|
|
13308
|
-
const body =
|
|
13446
|
+
const body = normalizeMailSnippetForDisplay(row.snippet, 2000);
|
|
13309
13447
|
return [
|
|
13310
13448
|
`Письмо #${row.uid}`,
|
|
13311
13449
|
`От: ${row.from || "-"}`,
|
|
@@ -13316,6 +13454,35 @@ function formatYandexMailRead(row) {
|
|
|
13316
13454
|
].filter((line) => line !== "").join("\n");
|
|
13317
13455
|
}
|
|
13318
13456
|
|
|
13457
|
+
function extractRecentMailHours(question) {
|
|
13458
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
13459
|
+
if (!/(последн|прошедш|за\s+\d+|за\s+два|за\s+три|за\s+час)/iu.test(text)) return 0;
|
|
13460
|
+
if (!/(час|минут|сут|день|дня|дней)/iu.test(text)) return 0;
|
|
13461
|
+
const numeric = Number(text.match(/(\d+)\s*(?:час|часа|часов)/iu)?.[1] || 0);
|
|
13462
|
+
if (numeric > 0) return numeric;
|
|
13463
|
+
const words = { один: 1, одну: 1, два: 2, две: 2, три: 3, четыре: 4, пять: 5, шесть: 6, семь: 7, восемь: 8, девять: 9, десять: 10 };
|
|
13464
|
+
const word = text.match(/(?:^|\s)(один|одну|два|две|три|четыре|пять|шесть|семь|восемь|девять|десять)\s*(?:час|часа|часов)(?:\s|$|[,.!?;:])/iu)?.[1];
|
|
13465
|
+
if (word && words[word]) return words[word];
|
|
13466
|
+
const minutes = Number(text.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 0);
|
|
13467
|
+
if (minutes > 0) return Math.max(1, Math.ceil(minutes / 60));
|
|
13468
|
+
if (/сутк|день|дня|дней/u.test(text)) return 24;
|
|
13469
|
+
return /час/u.test(text) ? 1 : 0;
|
|
13470
|
+
}
|
|
13471
|
+
|
|
13472
|
+
function formatRussianHours(value) {
|
|
13473
|
+
const n = Math.abs(Number(value || 0));
|
|
13474
|
+
if (n % 10 === 1 && n % 100 !== 11) return "час";
|
|
13475
|
+
if ([2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100)) return "часа";
|
|
13476
|
+
return "часов";
|
|
13477
|
+
}
|
|
13478
|
+
|
|
13479
|
+
function formatRussianLetters(value) {
|
|
13480
|
+
const n = Math.abs(Number(value || 0));
|
|
13481
|
+
if (n % 10 === 1 && n % 100 !== 11) return "письмо";
|
|
13482
|
+
if ([2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100)) return "письма";
|
|
13483
|
+
return "писем";
|
|
13484
|
+
}
|
|
13485
|
+
|
|
13319
13486
|
function slugForFile(value) {
|
|
13320
13487
|
return String(value || "")
|
|
13321
13488
|
.toLocaleLowerCase("ru-RU")
|
|
@@ -21,7 +21,19 @@ iola master
|
|
|
21
21
|
14. Yandex Cloud Connector - геокодинг и YandexGPT
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
CLI
|
|
24
|
+
CLI не просто открывает консоль. Он печатает короткую инструкцию, что делать дальше, и только потом просит вставить ключи.
|
|
25
|
+
|
|
26
|
+
Что делает пользователь:
|
|
27
|
+
|
|
28
|
+
1. Входит в свой Яндекс-аккаунт.
|
|
29
|
+
2. Если Яндекс просит создать облако или каталог, создает их. Названия можно оставить простыми: `iola-cli` и `default`.
|
|
30
|
+
3. Для геокодера открывает Кабинет разработчика: `https://developer.tech.yandex.ru/services/2`.
|
|
31
|
+
4. Выбирает `API Геокодера`, создает API key и копирует значение ключа.
|
|
32
|
+
5. Возвращается в окно CLI и вставляет ключ в поле `YANDEX_GEOCODER_API_KEY`.
|
|
33
|
+
6. Если нужен YandexGPT, в Yandex Cloud копирует ID каталога, создает сервисный аккаунт и API-ключ с правом `yc.ai.foundationModels.execute`.
|
|
34
|
+
7. Если YandexGPT пока не нужен, на вопрос CLI можно ответить `n` и подключить модель позже через `/model`.
|
|
35
|
+
|
|
36
|
+
CLI попросит сохранить:
|
|
25
37
|
|
|
26
38
|
- ключ `YANDEX_GEOCODER_API_KEY`;
|
|
27
39
|
- при необходимости YandexGPT API key;
|
|
@@ -101,6 +101,8 @@ iola index folder ./docs
|
|
|
101
101
|
|
|
102
102
|
Геокодер включается по умолчанию. Он нужен для geo-skills и deeplink Яндекс Go. YandexGPT можно включить сразу или выбрать позже через `/model`.
|
|
103
103
|
|
|
104
|
+
При запуске CLI печатает пошаговую инструкцию: войти в Яндекс, создать облако/каталог при необходимости, открыть Кабинет разработчика геокодера, создать API key, вернуться в CLI и вставить ключ. Пользователь не остается один на пустой странице консоли.
|
|
105
|
+
|
|
104
106
|
Ключи сохраняются локально у пользователя. Российский AI-провайдер вызывается напрямую, без gateway/proxy.
|
|
105
107
|
|
|
106
108
|
Инструкция: [Yandex Cloud Connector](Yandex-Cloud-Connector).
|