@iola_adm/iola-cli 0.2.35 → 0.2.37
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 +11 -7
- package/package.json +1 -1
- package/skills/yandex-services/SKILL.md +19 -3
- package/src/cli.js +1055 -46
- package/wiki/AI-/320/277/321/200/320/276/321/204/320/270/320/273/320/270.md +12 -1
- package/wiki/Daemon-RPC-/320/270-cron.md +47 -29
- package/wiki/Home.md +2 -0
- package/wiki/Skills-/320/270-toolsets.md +17 -1
- package/wiki/Yandex-Cloud-Connector.md +127 -0
- package/wiki/Yandex-Connector.md +63 -6
- package/wiki/Yandex-Geocoder-API-key.md +7 -1
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +14 -0
- package/wiki//320/234/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +10 -14
- package/wiki//320/240/320/265/321/210/320/265/320/275/320/270/320/265-/320/277/321/200/320/276/320/261/320/273/320/265/320/274.md +2 -2
- package/wiki//320/241/320/272/320/270/320/273/320/273/321/213-/320/264/320/273/321/217-/320/266/320/270/321/202/320/265/320/273/320/265/320/271.md +54 -3
package/src/cli.js
CHANGED
|
@@ -98,25 +98,25 @@ const YANDEX_CONNECTOR_SERVICES = {
|
|
|
98
98
|
hint: "встречи через календарное событие, если поддерживается",
|
|
99
99
|
},
|
|
100
100
|
cloud: {
|
|
101
|
-
title: "Yandex Cloud",
|
|
101
|
+
title: "Yandex Cloud Connector",
|
|
102
102
|
category: "cloud-platform",
|
|
103
103
|
scope: "",
|
|
104
|
-
status: "
|
|
105
|
-
hint: "
|
|
104
|
+
status: "ready",
|
|
105
|
+
hint: "геокодинг и YandexGPT через ключи Yandex Cloud",
|
|
106
106
|
},
|
|
107
107
|
maps: {
|
|
108
|
-
title: "Яндекс
|
|
108
|
+
title: "Яндекс Геокодер",
|
|
109
109
|
category: "maps",
|
|
110
110
|
scope: "",
|
|
111
|
-
status: "
|
|
112
|
-
hint: "
|
|
111
|
+
status: "ready",
|
|
112
|
+
hint: "адреса, координаты, маршруты и ссылки на карты",
|
|
113
113
|
},
|
|
114
114
|
taxi: {
|
|
115
115
|
title: "Яндекс Go / Такси",
|
|
116
116
|
category: "mobility",
|
|
117
117
|
scope: "",
|
|
118
|
-
status: "
|
|
119
|
-
hint: "
|
|
118
|
+
status: "ready",
|
|
119
|
+
hint: "deeplink и маршрут; заказ через API ожидает clid/apikey",
|
|
120
120
|
},
|
|
121
121
|
market: {
|
|
122
122
|
title: "Яндекс Маркет",
|
|
@@ -151,7 +151,7 @@ const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
|
151
151
|
const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open", "get_current_date"];
|
|
152
152
|
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
153
153
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
154
|
-
const USER_SKILL_TOOLS = ["user_skill_create", "user_skill_enable", "user_skill_disable", "user_skill_delete", "user_skill_list"];
|
|
154
|
+
const USER_SKILL_TOOLS = ["user_skill_create", "user_skill_update", "user_skill_enable", "user_skill_disable", "user_skill_delete", "user_skill_list", "user_skill_templates", "user_skill_validate", "user_skill_preview"];
|
|
155
155
|
const YANDEX_TOOLS = [
|
|
156
156
|
"yandex_identity_me",
|
|
157
157
|
"yandex_disk_info",
|
|
@@ -192,6 +192,7 @@ const YANDEX_TOOLS = [
|
|
|
192
192
|
"yandex_mail_city_context",
|
|
193
193
|
"yandex_mail_map_addresses",
|
|
194
194
|
"yandex_mail_create_task",
|
|
195
|
+
"yandex_mail_meeting_pack",
|
|
195
196
|
"yandex_calendar_status",
|
|
196
197
|
"yandex_calendar_calendars",
|
|
197
198
|
"yandex_calendar_create_event",
|
|
@@ -243,6 +244,12 @@ const YANDEX_TOOLS = [
|
|
|
243
244
|
"yandex_contact_from_public_entity",
|
|
244
245
|
"yandex_telemost_status",
|
|
245
246
|
"yandex_telemost_create_event",
|
|
247
|
+
"yandex_contact_full_pack",
|
|
248
|
+
"yandex_daily_digest",
|
|
249
|
+
"yandex_calendar_reminders_tick",
|
|
250
|
+
"yandex_disk_maintenance_tick",
|
|
251
|
+
"yandex_cloud_status",
|
|
252
|
+
"yandex_go_deeplink",
|
|
246
253
|
];
|
|
247
254
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
|
|
248
255
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
@@ -3065,6 +3072,29 @@ async function handleSkills(args) {
|
|
|
3065
3072
|
return;
|
|
3066
3073
|
}
|
|
3067
3074
|
|
|
3075
|
+
if (action === "templates") {
|
|
3076
|
+
printTable(userSkillTemplates().map((row) => ({ name: row.name, description: row.description, tools: row.tools.join(", ") })), [["name", "Шаблон"], ["description", "Описание"], ["tools", "Tools"]]);
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
if (action === "preview") {
|
|
3081
|
+
const options = parseOptions(args.slice(2));
|
|
3082
|
+
console.log(buildUserSkillPreview({
|
|
3083
|
+
name,
|
|
3084
|
+
description: options.description || "",
|
|
3085
|
+
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3086
|
+
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3087
|
+
template: options.template,
|
|
3088
|
+
}));
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
if (action === "validate") {
|
|
3093
|
+
const result = await userSkillValidate(name);
|
|
3094
|
+
printTable(result.checks, [["check", "Проверка"], ["status", "Статус"], ["message", "Сообщение"]]);
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3068
3098
|
if (action === "create" || action === "new") {
|
|
3069
3099
|
const options = parseOptions(args.slice(2));
|
|
3070
3100
|
const result = await userSkillCreate({
|
|
@@ -3072,6 +3102,7 @@ async function handleSkills(args) {
|
|
|
3072
3102
|
description: options.description || "",
|
|
3073
3103
|
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3074
3104
|
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3105
|
+
template: options.template,
|
|
3075
3106
|
enable: Boolean(options.enable),
|
|
3076
3107
|
overwrite: Boolean(options.force),
|
|
3077
3108
|
confirm: true,
|
|
@@ -3082,6 +3113,20 @@ async function handleSkills(args) {
|
|
|
3082
3113
|
return;
|
|
3083
3114
|
}
|
|
3084
3115
|
|
|
3116
|
+
if (action === "update" || action === "edit") {
|
|
3117
|
+
const options = parseOptions(args.slice(2));
|
|
3118
|
+
const result = await userSkillUpdate(name, {
|
|
3119
|
+
description: options.description,
|
|
3120
|
+
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3121
|
+
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3122
|
+
enable: options.enable,
|
|
3123
|
+
confirm: true,
|
|
3124
|
+
});
|
|
3125
|
+
console.log(`Skill обновлен: ${result.name}`);
|
|
3126
|
+
console.log(`Файл: ${result.file}`);
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3085
3130
|
if (action === "delete" || action === "remove" || action === "rm") {
|
|
3086
3131
|
const options = parseOptions(args.slice(2));
|
|
3087
3132
|
const result = await userSkillDelete(name, { confirm: Boolean(options.yes || options.force) });
|
|
@@ -3099,7 +3144,7 @@ async function handleSkills(args) {
|
|
|
3099
3144
|
return;
|
|
3100
3145
|
}
|
|
3101
3146
|
|
|
3102
|
-
throw new Error("Команды skills: list, paths, show NAME, create NAME --description TEXT --instructions TEXT [--enable], enable NAME, disable NAME, delete NAME --yes, bundles, bundle enable NAME, doctor.");
|
|
3147
|
+
throw new Error("Команды skills: list, paths, show NAME, templates, preview NAME --template T, create NAME --description TEXT --instructions TEXT [--enable], update NAME --instructions TEXT, validate NAME, enable NAME, disable NAME, delete NAME --yes, bundles, bundle enable NAME, doctor.");
|
|
3103
3148
|
}
|
|
3104
3149
|
|
|
3105
3150
|
async function handleTools(args) {
|
|
@@ -3390,11 +3435,36 @@ async function handleYandex(args) {
|
|
|
3390
3435
|
return;
|
|
3391
3436
|
}
|
|
3392
3437
|
|
|
3438
|
+
if (action === "cloud" || action === "cloud-connector" || action === "yc") {
|
|
3439
|
+
await handleYandexCloudConnector([target, ...rest].filter(Boolean));
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
if (action === "go" || action === "taxi" || action === "такси") {
|
|
3444
|
+
await handleYandexGo([target, ...rest].filter(Boolean));
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3393
3448
|
if (action === "mail-watch" || action === "mailwatch" || action === "watch-mail") {
|
|
3394
3449
|
await handleYandexMailWatch([target, ...rest].filter(Boolean));
|
|
3395
3450
|
return;
|
|
3396
3451
|
}
|
|
3397
3452
|
|
|
3453
|
+
if (action === "daily-digest" || action === "digest") {
|
|
3454
|
+
await handleYandexDailyDigest([target, ...rest].filter(Boolean));
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
if (action === "calendar-reminders" || action === "calendar-watch" || action === "reminders") {
|
|
3459
|
+
await handleYandexCalendarReminders([target, ...rest].filter(Boolean));
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
if (action === "disk-maintenance" || action === "disk-watch" || action === "disk-doctor") {
|
|
3464
|
+
await handleYandexDiskMaintenance([target, ...rest].filter(Boolean));
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3398
3468
|
if (action === "contacts-maintenance" || action === "contacts-watch" || action === "contacts-doctor") {
|
|
3399
3469
|
await handleYandexContactsMaintenance([target, ...rest].filter(Boolean));
|
|
3400
3470
|
return;
|
|
@@ -3435,7 +3505,15 @@ async function handleYandex(args) {
|
|
|
3435
3505
|
iola yandex menu
|
|
3436
3506
|
iola yandex status|doctor
|
|
3437
3507
|
iola yandex services
|
|
3508
|
+
iola yandex cloud setup|status|doctor|delete
|
|
3509
|
+
iola yandex cloud enable geocoder yandexgpt
|
|
3510
|
+
iola yandex cloud disable yandexgpt
|
|
3511
|
+
iola yandex go link --from "Адрес" --to "Адрес" [--tariff econom]
|
|
3512
|
+
iola yandex go open --from "Адрес" --to "Адрес" [--tariff econom]
|
|
3438
3513
|
iola yandex mail-watch on|off|status|tick [--minutes 5]
|
|
3514
|
+
iola yandex daily-digest on|off|status|tick [--time 09:00] [--email]
|
|
3515
|
+
iola yandex calendar-reminders on|off|status|tick [--minutes 15]
|
|
3516
|
+
iola yandex disk-maintenance on|off|status|tick [--days 7]
|
|
3439
3517
|
iola yandex contacts-maintenance on|off|status|tick [--days 7] [--backup]
|
|
3440
3518
|
iola yandex enable disk mail calendar
|
|
3441
3519
|
iola yandex disable mail
|
|
@@ -3466,6 +3544,332 @@ function printYandexServices(options = {}) {
|
|
|
3466
3544
|
]);
|
|
3467
3545
|
}
|
|
3468
3546
|
|
|
3547
|
+
async function handleYandexCloudConnector(args = []) {
|
|
3548
|
+
const [action = "status", ...rest] = args;
|
|
3549
|
+
const options = parseOptions(rest);
|
|
3550
|
+
|
|
3551
|
+
if (action === "setup" || action === "connect" || action === "onboard") {
|
|
3552
|
+
await setupYandexCloudConnector(options);
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
if (action === "status" || action === "doctor" || action === "check") {
|
|
3557
|
+
await printYandexCloudConnectorStatus({ check: action !== "status" || options.check });
|
|
3558
|
+
return;
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
if (action === "enable" || action === "disable") {
|
|
3562
|
+
const services = options._.length ? options._ : rest.filter((item) => item && !String(item).startsWith("--"));
|
|
3563
|
+
await updateYandexCloudEnabledServices(services, action === "enable");
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
if (action === "delete" || action === "disconnect" || action === "remove") {
|
|
3568
|
+
const ok = !process.stdin.isTTY || await askYesNo("Удалить локальные ключи и настройки Yandex Cloud Connector? [y/N] ", false);
|
|
3569
|
+
if (!ok) {
|
|
3570
|
+
console.log("Удаление отменено.");
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
await deleteYandexCloudConnector();
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
if (action === "open") {
|
|
3578
|
+
await openUrl("https://console.yandex.cloud/");
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
throw new Error("Команды: iola yandex cloud setup | status | doctor | enable geocoder yandexgpt | disable yandexgpt | delete");
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
async function setupYandexCloudConnector(options = {}) {
|
|
3586
|
+
console.log("Yandex Cloud Connector: геокодинг и YandexGPT.");
|
|
3587
|
+
console.log("Геокодер будет включен по умолчанию. YandexGPT можно выбрать в /model после сохранения ключей.");
|
|
3588
|
+
if (process.stdin.isTTY) {
|
|
3589
|
+
const openConsole = await askYesNo("Открыть Yandex Cloud Console для получения ключей? [Y/n] ", true);
|
|
3590
|
+
if (openConsole) await openUrl("https://console.yandex.cloud/");
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
const secrets = await loadSecrets();
|
|
3594
|
+
const currentGeocoder = secrets.yandexGeocoder?.apiKey || secrets.yandexCloud?.geocoderApiKey || "";
|
|
3595
|
+
const currentGptKey = secrets.yandexgpt?.apiKey || secrets.yandexCloud?.yandexgptApiKey || "";
|
|
3596
|
+
const currentFolderId = secrets.yandexgpt?.folderId || secrets.yandexCloud?.folderId || "";
|
|
3597
|
+
|
|
3598
|
+
if (!process.stdin.isTTY) {
|
|
3599
|
+
await saveYandexCloudEnabledServices(["geocoder"]);
|
|
3600
|
+
console.log("Интерактивный ввод ключей недоступен. Запустите: iola yandex cloud setup");
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
const geocoderKey = (await askText(`YANDEX_GEOCODER_API_KEY${currentGeocoder ? " [уже сохранен, Enter - оставить]" : ""}: `)).trim() || currentGeocoder;
|
|
3605
|
+
if (!geocoderKey) throw new Error("Для Cloud Connector нужен хотя бы Geocoder API key.");
|
|
3606
|
+
|
|
3607
|
+
const setupGpt = await askYesNo(`Настроить YandexGPT сейчас${currentGptKey && currentFolderId ? " (уже сохранен)" : ""}? [y/N] `, Boolean(currentGptKey && currentFolderId));
|
|
3608
|
+
let yandexgptApiKey = currentGptKey;
|
|
3609
|
+
let folderId = currentFolderId;
|
|
3610
|
+
if (setupGpt) {
|
|
3611
|
+
yandexgptApiKey = (await askText(`YANDEXGPT_API_KEY${currentGptKey ? " [Enter - оставить]" : ""}: `)).trim() || currentGptKey;
|
|
3612
|
+
folderId = (await askText(`YANDEXGPT_FOLDER_ID${currentFolderId ? " [Enter - оставить]" : ""}: `)).trim() || currentFolderId;
|
|
3613
|
+
if (!yandexgptApiKey || !folderId) throw new Error("Для YandexGPT нужны API key и folder ID.");
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
await saveYandexCloudConnectorSecrets({ geocoderApiKey: geocoderKey, yandexgptApiKey, folderId });
|
|
3617
|
+
await saveYandexCloudEnabledServices(setupGpt ? ["geocoder", "yandexgpt"] : ["geocoder"]);
|
|
3618
|
+
console.log(`Yandex Cloud Connector сохранен локально: ${SECRETS_FILE}`);
|
|
3619
|
+
await printYandexCloudConnectorStatus({ check: true });
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
async function saveYandexCloudConnectorSecrets({ geocoderApiKey, yandexgptApiKey, folderId }) {
|
|
3623
|
+
const secrets = await loadSecrets();
|
|
3624
|
+
secrets.yandexCloud = {
|
|
3625
|
+
...(secrets.yandexCloud || {}),
|
|
3626
|
+
geocoderApiKey: geocoderApiKey || secrets.yandexCloud?.geocoderApiKey || "",
|
|
3627
|
+
yandexgptApiKey: yandexgptApiKey || secrets.yandexCloud?.yandexgptApiKey || "",
|
|
3628
|
+
folderId: folderId || secrets.yandexCloud?.folderId || "",
|
|
3629
|
+
updatedAt: new Date().toISOString(),
|
|
3630
|
+
};
|
|
3631
|
+
if (geocoderApiKey) secrets.yandexGeocoder = { ...(secrets.yandexGeocoder || {}), apiKey: geocoderApiKey };
|
|
3632
|
+
if (yandexgptApiKey || folderId) {
|
|
3633
|
+
secrets.yandexgpt = {
|
|
3634
|
+
...(secrets.yandexgpt || {}),
|
|
3635
|
+
apiKey: yandexgptApiKey || secrets.yandexgpt?.apiKey || "",
|
|
3636
|
+
folderId: folderId || secrets.yandexgpt?.folderId || "",
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
await saveSecrets(secrets);
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
async function saveYandexCloudEnabledServices(services) {
|
|
3643
|
+
const config = await loadConfig();
|
|
3644
|
+
const normalized = normalizeYandexCloudServiceList(services);
|
|
3645
|
+
await saveConfig({
|
|
3646
|
+
yandex: {
|
|
3647
|
+
...(config.yandex || {}),
|
|
3648
|
+
cloudConnector: {
|
|
3649
|
+
...(config.yandex?.cloudConnector || {}),
|
|
3650
|
+
enabledServices: normalized,
|
|
3651
|
+
updatedAt: new Date().toISOString(),
|
|
3652
|
+
},
|
|
3653
|
+
},
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
async function updateYandexCloudEnabledServices(rawServices, enabled) {
|
|
3658
|
+
const config = await loadConfig();
|
|
3659
|
+
const current = new Set(config.yandex?.cloudConnector?.enabledServices || ["geocoder"]);
|
|
3660
|
+
for (const service of normalizeYandexCloudServiceList(rawServices)) {
|
|
3661
|
+
if (enabled) current.add(service);
|
|
3662
|
+
else current.delete(service);
|
|
3663
|
+
}
|
|
3664
|
+
if (current.size > 0 && !current.has("geocoder")) current.add("geocoder");
|
|
3665
|
+
await saveYandexCloudEnabledServices([...current]);
|
|
3666
|
+
console.log(`Yandex Cloud services: ${[...current].join(", ") || "-"}`);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
function normalizeYandexCloudServiceList(services = []) {
|
|
3670
|
+
const aliases = {
|
|
3671
|
+
geo: "geocoder",
|
|
3672
|
+
geocoder: "geocoder",
|
|
3673
|
+
maps: "geocoder",
|
|
3674
|
+
map: "geocoder",
|
|
3675
|
+
"yandex-geocoder": "geocoder",
|
|
3676
|
+
gpt: "yandexgpt",
|
|
3677
|
+
yandexgpt: "yandexgpt",
|
|
3678
|
+
"yandex-gpt": "yandexgpt",
|
|
3679
|
+
model: "yandexgpt",
|
|
3680
|
+
models: "yandexgpt",
|
|
3681
|
+
};
|
|
3682
|
+
return [...new Set([].concat(services || []).map((item) => aliases[String(item || "").toLocaleLowerCase("ru-RU")] || "").filter(Boolean))];
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
async function printYandexCloudConnectorStatus(options = {}) {
|
|
3686
|
+
const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
|
|
3687
|
+
const enabled = new Set(config.yandex?.cloudConnector?.enabledServices || []);
|
|
3688
|
+
const geocoderKey = process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey || "";
|
|
3689
|
+
const gptKey = process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey || "";
|
|
3690
|
+
const folderId = process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId || "";
|
|
3691
|
+
const rows = [
|
|
3692
|
+
{ service: "geocoder", enabled: enabled.has("geocoder") ? "yes" : "no", configured: geocoderKey ? "yes" : "no", source: geocoderKey ? "local/env" : "-", hint: "адреса и координаты" },
|
|
3693
|
+
{ service: "yandexgpt", enabled: enabled.has("yandexgpt") ? "yes" : "no", configured: gptKey && folderId ? "yes" : "no", source: gptKey && folderId ? "local/env" : "-", hint: "модели YandexGPT" },
|
|
3694
|
+
];
|
|
3695
|
+
printTable(rows, [
|
|
3696
|
+
["service", "Сервис"],
|
|
3697
|
+
["enabled", "Вкл"],
|
|
3698
|
+
["configured", "Настроен"],
|
|
3699
|
+
["source", "Ключ"],
|
|
3700
|
+
["hint", "Суть"],
|
|
3701
|
+
]);
|
|
3702
|
+
if (options.check) {
|
|
3703
|
+
await checkYandexGeocoderKey({ print: true });
|
|
3704
|
+
if (gptKey && folderId) console.log("YandexGPT: ключ и folder ID найдены.");
|
|
3705
|
+
else console.log("YandexGPT: не настроен.");
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
async function deleteYandexCloudConnector() {
|
|
3710
|
+
const secrets = await loadSecrets();
|
|
3711
|
+
delete secrets.yandexCloud;
|
|
3712
|
+
delete secrets.yandexGeocoder;
|
|
3713
|
+
delete secrets.yandexgpt;
|
|
3714
|
+
await saveSecrets(secrets);
|
|
3715
|
+
const config = await loadConfig();
|
|
3716
|
+
await saveConfig({
|
|
3717
|
+
yandex: {
|
|
3718
|
+
...(config.yandex || {}),
|
|
3719
|
+
cloudConnector: { enabledServices: [], updatedAt: new Date().toISOString() },
|
|
3720
|
+
},
|
|
3721
|
+
});
|
|
3722
|
+
console.log("Yandex Cloud Connector удален локально.");
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
async function handleYandexGo(args = []) {
|
|
3726
|
+
const [action = "link", ...rest] = args;
|
|
3727
|
+
const options = parseOptions(rest);
|
|
3728
|
+
if (action === "link" || action === "deeplink" || action === "url") {
|
|
3729
|
+
await ensureYandexGoGeocoderReady();
|
|
3730
|
+
const result = await buildYandexGoDeeplinkFromOptions(options);
|
|
3731
|
+
printYandexGoDeeplinkResult(result);
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (action === "open" || action === "prepare" || action === "route") {
|
|
3735
|
+
await ensureYandexGoGeocoderReady();
|
|
3736
|
+
const result = await buildYandexGoDeeplinkFromOptions(options);
|
|
3737
|
+
printYandexGoDeeplinkResult(result);
|
|
3738
|
+
await openUrl(result.url);
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
if (action === "status") {
|
|
3742
|
+
printKeyValue({
|
|
3743
|
+
deeplink: "ready",
|
|
3744
|
+
priceApi: "ожидает clid/apikey от Яндекса",
|
|
3745
|
+
orderApi: "не подключен",
|
|
3746
|
+
});
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
throw new Error('Команды: iola yandex go link --from "Адрес" --to "Адрес" [--tariff econom] | open --from "Адрес" --to "Адрес"');
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
async function ensureYandexGoGeocoderReady() {
|
|
3753
|
+
if (await getYandexGeocoderKey()) return true;
|
|
3754
|
+
throw new Error([
|
|
3755
|
+
"Для Yandex Go deeplink нужен ключ Yandex Geocoder API: адреса нужно превратить в координаты.",
|
|
3756
|
+
"Откройте мастер настройки и запустите Yandex Cloud Connector (геокодинг и YandexGPT):",
|
|
3757
|
+
" iola master",
|
|
3758
|
+
"или напрямую:",
|
|
3759
|
+
" iola yandex cloud setup",
|
|
3760
|
+
"После подключения повторите команду такси.",
|
|
3761
|
+
].join("\n"));
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
async function buildYandexGoDeeplinkFromOptions(options = {}) {
|
|
3765
|
+
const from = options.from || options._?.[0] || "";
|
|
3766
|
+
const to = options.to || options._?.[1] || "";
|
|
3767
|
+
if (!from || !to) throw new Error('Укажите маршрут: iola yandex go link --from "Медведево, Школьная 15" --to "Медведево, Советская 20"');
|
|
3768
|
+
const fromPoint = await resolveYandexGoPoint(from);
|
|
3769
|
+
const toPoint = await resolveYandexGoPoint(to);
|
|
3770
|
+
const tariff = normalizeYandexGoTariff(options.tariff || options.class || options.level || "econom");
|
|
3771
|
+
return {
|
|
3772
|
+
from,
|
|
3773
|
+
to,
|
|
3774
|
+
fromPoint,
|
|
3775
|
+
toPoint,
|
|
3776
|
+
tariff,
|
|
3777
|
+
url: buildYandexGoDeeplink({
|
|
3778
|
+
fromPoint,
|
|
3779
|
+
toPoint,
|
|
3780
|
+
tariff,
|
|
3781
|
+
ref: options.ref || "iola-cli",
|
|
3782
|
+
lang: options.lang || "ru",
|
|
3783
|
+
}),
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
async function resolveYandexGoPoint(query) {
|
|
3788
|
+
const parsed = parseLonLat(query);
|
|
3789
|
+
if (parsed) return { lon: parsed.lon, lat: parsed.lat, label: `${parsed.lat}, ${parsed.lon}`, address: "" };
|
|
3790
|
+
const point = await callYandexGeocoder(query);
|
|
3791
|
+
const coords = parseCoordinates(point?.coordinates);
|
|
3792
|
+
if (!Number.isFinite(coords.lat) || !Number.isFinite(coords.lon)) throw new Error(`Не смог получить координаты: ${query}`);
|
|
3793
|
+
return { lon: coords.lon, lat: coords.lat, label: point.name || query, address: point.address || "" };
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
function parseLonLat(value) {
|
|
3797
|
+
const text = String(value || "").trim();
|
|
3798
|
+
const match = text.match(/^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/u);
|
|
3799
|
+
if (!match) return null;
|
|
3800
|
+
const first = Number(match[1]);
|
|
3801
|
+
const second = Number(match[2]);
|
|
3802
|
+
if (!Number.isFinite(first) || !Number.isFinite(second)) return null;
|
|
3803
|
+
if (Math.abs(first) <= 90 && Math.abs(second) > 90) return { lat: first, lon: second };
|
|
3804
|
+
return { lon: first, lat: second };
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
function normalizeYandexGoTariff(value) {
|
|
3808
|
+
const text = String(value || "").toLocaleLowerCase("ru-RU").replace(/\s+/g, "");
|
|
3809
|
+
const aliases = {
|
|
3810
|
+
economy: "econom",
|
|
3811
|
+
econom: "econom",
|
|
3812
|
+
эконом: "econom",
|
|
3813
|
+
comfort: "business",
|
|
3814
|
+
комфорт: "business",
|
|
3815
|
+
business: "business",
|
|
3816
|
+
comfortplus: "comfortplus",
|
|
3817
|
+
"комфорт+": "comfortplus",
|
|
3818
|
+
komfortplus: "comfortplus",
|
|
3819
|
+
minivan: "minivan",
|
|
3820
|
+
минивен: "minivan",
|
|
3821
|
+
vip: "vip",
|
|
3822
|
+
бизнес: "vip",
|
|
3823
|
+
детский: "econom",
|
|
3824
|
+
child: "econom",
|
|
3825
|
+
children: "econom",
|
|
3826
|
+
};
|
|
3827
|
+
return aliases[text] || "econom";
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
function buildYandexGoDeeplink({ fromPoint, toPoint, tariff = "econom", ref = "iola-cli", lang = "ru" }) {
|
|
3831
|
+
const url = new URL("https://3.redirect.appmetrica.yandex.com/route");
|
|
3832
|
+
url.searchParams.set("start-lat", String(fromPoint.lat));
|
|
3833
|
+
url.searchParams.set("start-lon", String(fromPoint.lon));
|
|
3834
|
+
url.searchParams.set("end-lat", String(toPoint.lat));
|
|
3835
|
+
url.searchParams.set("end-lon", String(toPoint.lon));
|
|
3836
|
+
url.searchParams.set("tariffClass", tariff);
|
|
3837
|
+
url.searchParams.set("level", tariff);
|
|
3838
|
+
url.searchParams.set("ref", ref);
|
|
3839
|
+
url.searchParams.set("lang", lang);
|
|
3840
|
+
url.searchParams.set("appmetrica_tracking_id", "1178268795219780156");
|
|
3841
|
+
return url.toString();
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
function printYandexGoDeeplinkResult(result) {
|
|
3845
|
+
console.log(formatYandexGoDeeplinkResult(result));
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
function formatYandexGoDeeplinkResult(result) {
|
|
3849
|
+
return [
|
|
3850
|
+
`Откуда: ${result.fromPoint.address || result.from}`,
|
|
3851
|
+
`Куда: ${result.toPoint.address || result.to}`,
|
|
3852
|
+
`Тариф: ${result.tariff}`,
|
|
3853
|
+
`Ссылка Яндекс Go: ${result.url}`,
|
|
3854
|
+
"Детское кресло и повышенный спрос deeplink не кодирует напрямую; это выбирается/проверяется в интерфейсе Яндекс Go или через taxi_info после получения clid/apikey.",
|
|
3855
|
+
].join("\n");
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
function extractYandexGoRouteFromText(text) {
|
|
3859
|
+
const source = String(text || "").trim();
|
|
3860
|
+
const tariffMatch = source.match(/(эконом|комфорт\+?|комфорт плюс|бизнес|минивен|детск\w*|econom|business|comfortplus|minivan|vip)/iu);
|
|
3861
|
+
const cleaned = source
|
|
3862
|
+
.replace(/^(?:построй|создай|дай|открой|сделай|подготовь|вызови|закажи)\s+/iu, "")
|
|
3863
|
+
.replace(/\b(?:яндекс\s*go|яндекс\s*го|такси|маршрут|ссылк[ау]?|диплинк|deeplink)\b/giu, " ")
|
|
3864
|
+
.replace(/\s+/g, " ")
|
|
3865
|
+
.trim();
|
|
3866
|
+
const match = cleaned.match(/(?:от|из|с)\s+(.+?)\s+(?:до|в|на)\s+(.+)$/iu);
|
|
3867
|
+
if (!match) return { from: "", to: "", tariff: tariffMatch ? normalizeYandexGoTariff(tariffMatch[1]) : "econom" };
|
|
3868
|
+
const from = match[1].replace(/[,.;]\s*$/u, "").trim();
|
|
3869
|
+
const to = match[2].replace(/[,.;]\s*(?:тариф|эконом|комфорт\+?|комфорт плюс|бизнес|минивен|детск\w*).*$/iu, "").trim();
|
|
3870
|
+
return { from, to, tariff: tariffMatch ? normalizeYandexGoTariff(tariffMatch[1]) : "econom" };
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3469
3873
|
async function handleYandexMailWatch(args = []) {
|
|
3470
3874
|
const [action = "status", ...rest] = args;
|
|
3471
3875
|
const options = parseOptions(rest);
|
|
@@ -3507,6 +3911,107 @@ async function handleYandexMailWatch(args = []) {
|
|
|
3507
3911
|
throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
|
|
3508
3912
|
}
|
|
3509
3913
|
|
|
3914
|
+
async function handleYandexDailyDigest(args = []) {
|
|
3915
|
+
const [action = "status", ...rest] = args;
|
|
3916
|
+
const options = parseOptions(rest);
|
|
3917
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3918
|
+
const time = options.time || rest.find((item) => /^\d{1,2}:\d{2}$/u.test(String(item))) || "09:00";
|
|
3919
|
+
const result = await yandexDailyDigestEnable({ time, email: Boolean(options.email), save: options.save !== false });
|
|
3920
|
+
console.log(`Ежедневный дайджест включен: каждый день ${result.time}. Email: ${result.email ? "yes" : "no"}.`);
|
|
3921
|
+
return;
|
|
3922
|
+
}
|
|
3923
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3924
|
+
await yandexDailyDigestDisable();
|
|
3925
|
+
console.log("Ежедневный дайджест выключен.");
|
|
3926
|
+
return;
|
|
3927
|
+
}
|
|
3928
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3929
|
+
const result = await yandexDailyDigestTick({ force: true, email: Boolean(options.email), save: options.save !== false });
|
|
3930
|
+
console.log(result.text || "Дайджест пуст.");
|
|
3931
|
+
if (result.remote) console.log(`Сохранено на Диск: ${result.remote}`);
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
const config = await loadConfig();
|
|
3935
|
+
const digest = config.yandex?.dailyDigest || {};
|
|
3936
|
+
printKeyValue({
|
|
3937
|
+
enabled: digest.enabled ? "yes" : "no",
|
|
3938
|
+
time: digest.time || "-",
|
|
3939
|
+
email: digest.email ? "yes" : "no",
|
|
3940
|
+
lastRunAt: digest.lastRunAt || "-",
|
|
3941
|
+
lastRemote: digest.lastRemote || "-",
|
|
3942
|
+
cron: listCronJobs().some((job) => job.command === "yandex daily-digest tick") ? "yes" : "no",
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
async function handleYandexCalendarReminders(args = []) {
|
|
3947
|
+
const [action = "status", ...rest] = args;
|
|
3948
|
+
const options = parseOptions(rest);
|
|
3949
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3950
|
+
const minutes = Math.max(1, Number(options.minutes || rest.find((item) => /^\d+$/u.test(String(item))) || 15));
|
|
3951
|
+
const result = await yandexCalendarRemindersEnable(minutes);
|
|
3952
|
+
console.log(`Проверка календарных напоминаний включена: каждые ${result.minutes} минут.`);
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3956
|
+
await yandexCalendarRemindersDisable();
|
|
3957
|
+
console.log("Проверка календарных напоминаний выключена.");
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3961
|
+
const result = await yandexCalendarRemindersTick({ force: true, horizonMinutes: Number(options.horizon || options.minutes || 60) });
|
|
3962
|
+
if (!result.events.length) console.log("Ближайших событий для напоминания нет.");
|
|
3963
|
+
else console.log(["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n"));
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
const config = await loadConfig();
|
|
3967
|
+
const reminders = config.yandex?.calendarReminders || {};
|
|
3968
|
+
printKeyValue({
|
|
3969
|
+
enabled: reminders.enabled ? "yes" : "no",
|
|
3970
|
+
minutes: reminders.minutes || "-",
|
|
3971
|
+
horizonMinutes: reminders.horizonMinutes || 60,
|
|
3972
|
+
lastRunAt: reminders.lastRunAt || "-",
|
|
3973
|
+
lastCount: reminders.lastCount ?? "-",
|
|
3974
|
+
cron: listCronJobs().some((job) => job.command === "yandex calendar-reminders tick") ? "yes" : "no",
|
|
3975
|
+
});
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
async function handleYandexDiskMaintenance(args = []) {
|
|
3979
|
+
const [action = "status", ...rest] = args;
|
|
3980
|
+
const options = parseOptions(rest);
|
|
3981
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3982
|
+
const days = Math.max(1, Number(options.days || rest.find((item) => /^\d+$/u.test(String(item))) || 7));
|
|
3983
|
+
const result = await yandexDiskMaintenanceEnable(days);
|
|
3984
|
+
console.log(`Проверка Яндекс Диска включена: каждые ${result.days} дней.`);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3988
|
+
await yandexDiskMaintenanceDisable();
|
|
3989
|
+
console.log("Проверка Яндекс Диска выключена.");
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3993
|
+
const result = await yandexDiskMaintenanceTick({ force: true });
|
|
3994
|
+
printKeyValue({
|
|
3995
|
+
used: formatBytes(result.usedSpace),
|
|
3996
|
+
total: formatBytes(result.totalSpace),
|
|
3997
|
+
trash: formatBytes(result.trashSize),
|
|
3998
|
+
docs: result.docs,
|
|
3999
|
+
publicLinks: result.publicLinks,
|
|
4000
|
+
remote: result.remote || "-",
|
|
4001
|
+
});
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
const config = await loadConfig();
|
|
4005
|
+
const disk = config.yandex?.diskMaintenance || {};
|
|
4006
|
+
printKeyValue({
|
|
4007
|
+
enabled: disk.enabled ? "yes" : "no",
|
|
4008
|
+
days: disk.days || "-",
|
|
4009
|
+
lastRunAt: disk.lastRunAt || "-",
|
|
4010
|
+
lastRemote: disk.lastRemote || "-",
|
|
4011
|
+
cron: listCronJobs().some((job) => job.command === "yandex disk-maintenance tick") ? "yes" : "no",
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
|
|
3510
4015
|
async function handleYandexContactsMaintenance(args = []) {
|
|
3511
4016
|
const [action = "status", ...rest] = args;
|
|
3512
4017
|
const options = parseOptions(rest);
|
|
@@ -3615,9 +4120,12 @@ async function chooseYandexServicesMenu() {
|
|
|
3615
4120
|
}
|
|
3616
4121
|
const config = await loadConfig();
|
|
3617
4122
|
const serviceIds = getYandexConnectorMenuServiceIds();
|
|
3618
|
-
const
|
|
4123
|
+
const cloudNumber = serviceIds.length + 1;
|
|
4124
|
+
const goNumber = serviceIds.length + 2;
|
|
4125
|
+
const deleteNumber = serviceIds.length + 3;
|
|
3619
4126
|
const enabled = new Set(config.yandex?.enabledServices?.length ? config.yandex.enabledServices : ["identity", "disk"]);
|
|
3620
4127
|
const authState = await getYandexServiceAuthState();
|
|
4128
|
+
const cloudStatus = await getYandexCloudConnectorSummary();
|
|
3621
4129
|
console.log("Функции Яндекса.");
|
|
3622
4130
|
console.log("Выберите номера функций через запятую:");
|
|
3623
4131
|
serviceIds.forEach((id, index) => {
|
|
@@ -3627,6 +4135,8 @@ async function chooseYandexServicesMenu() {
|
|
|
3627
4135
|
const authLabel = auth?.hasToken ? "подключено" : (auth?.authorized ? "нужен вход" : "нет прав");
|
|
3628
4136
|
console.log(`${index + 1}. [${marker}] ${service.title} - ${service.hint} (${service.status}, ${authLabel})`);
|
|
3629
4137
|
});
|
|
4138
|
+
console.log(`${cloudNumber}. Yandex Cloud Connector - геокодинг и YandexGPT (${cloudStatus})`);
|
|
4139
|
+
console.log(`${goNumber}. Yandex Go / Такси - deeplink и маршрут (готово, заказ через API ожидает clid/apikey)`);
|
|
3630
4140
|
console.log(`${deleteNumber}. Удалить подключение-коннектор`);
|
|
3631
4141
|
console.log("0. Отмена");
|
|
3632
4142
|
const defaults = serviceIds.map((id, index) => enabled.has(id) ? String(index + 1) : "").filter(Boolean);
|
|
@@ -3646,6 +4156,17 @@ async function chooseYandexServicesMenu() {
|
|
|
3646
4156
|
await deleteYandexConnectorToken();
|
|
3647
4157
|
return;
|
|
3648
4158
|
}
|
|
4159
|
+
if (selectedNumbers.includes(String(cloudNumber))) {
|
|
4160
|
+
if (selectedNumbers.length > 1) throw new Error("Yandex Cloud Connector выбирается отдельно, без других пунктов.");
|
|
4161
|
+
await chooseYandexCloudConnectorMenu();
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
if (selectedNumbers.includes(String(goNumber))) {
|
|
4165
|
+
if (selectedNumbers.length > 1) throw new Error("Yandex Go выбирается отдельно, без других пунктов.");
|
|
4166
|
+
await handleYandexGo(["status"]);
|
|
4167
|
+
console.log('Для маршрута: iola yandex go open --from "Адрес" --to "Адрес" --tariff econom');
|
|
4168
|
+
return;
|
|
4169
|
+
}
|
|
3649
4170
|
const selected = selectedNumbers.map((item) => {
|
|
3650
4171
|
const index = Number(item) - 1;
|
|
3651
4172
|
if (!Number.isInteger(index) || index < 0 || index >= serviceIds.length) {
|
|
@@ -3663,10 +4184,51 @@ async function chooseYandexServicesMenu() {
|
|
|
3663
4184
|
|
|
3664
4185
|
function getYandexConnectorMenuServiceIds() {
|
|
3665
4186
|
return Object.entries(YANDEX_CONNECTOR_SERVICES)
|
|
3666
|
-
.filter(([, service]) => service.status === "ready" || service.status === "research")
|
|
4187
|
+
.filter(([, service]) => (service.status === "ready" || service.status === "research") && service.scope)
|
|
3667
4188
|
.map(([id]) => id);
|
|
3668
4189
|
}
|
|
3669
4190
|
|
|
4191
|
+
async function chooseYandexCloudConnectorMenu() {
|
|
4192
|
+
const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
|
|
4193
|
+
const enabled = new Set(config.yandex?.cloudConnector?.enabledServices || ["geocoder"]);
|
|
4194
|
+
const geocoderConfigured = Boolean(process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey);
|
|
4195
|
+
const gptConfigured = Boolean((process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey)
|
|
4196
|
+
&& (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId));
|
|
4197
|
+
console.log("Yandex Cloud Connector.");
|
|
4198
|
+
console.log("1. Настроить/обновить ключи");
|
|
4199
|
+
console.log(`2. [${enabled.has("geocoder") ? "✓" : " "}] Геокодер (${geocoderConfigured ? "ключ есть" : "ключ не задан"})`);
|
|
4200
|
+
console.log(`3. [${enabled.has("yandexgpt") ? "✓" : " "}] YandexGPT (${gptConfigured ? "ключ и folder ID есть" : "не настроено"})`);
|
|
4201
|
+
console.log("4. Проверить подключение");
|
|
4202
|
+
console.log("5. Удалить Cloud Connector");
|
|
4203
|
+
console.log("0. Назад");
|
|
4204
|
+
const answer = (await askText("Номер: ")).trim();
|
|
4205
|
+
if (answer === "0" || !answer) return;
|
|
4206
|
+
if (answer === "1") return setupYandexCloudConnector({});
|
|
4207
|
+
if (answer === "2") {
|
|
4208
|
+
if (enabled.has("geocoder")) await updateYandexCloudEnabledServices(["geocoder"], false);
|
|
4209
|
+
else await updateYandexCloudEnabledServices(["geocoder"], true);
|
|
4210
|
+
return;
|
|
4211
|
+
}
|
|
4212
|
+
if (answer === "3") {
|
|
4213
|
+
if (enabled.has("yandexgpt")) await updateYandexCloudEnabledServices(["yandexgpt"], false);
|
|
4214
|
+
else await updateYandexCloudEnabledServices(["yandexgpt"], true);
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
if (answer === "4") return printYandexCloudConnectorStatus({ check: true });
|
|
4218
|
+
if (answer === "5") return handleYandexCloudConnector(["delete"]);
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
async function getYandexCloudConnectorSummary() {
|
|
4222
|
+
const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
|
|
4223
|
+
const enabled = config.yandex?.cloudConnector?.enabledServices || [];
|
|
4224
|
+
const geocoder = Boolean(process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey);
|
|
4225
|
+
const gpt = Boolean((process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey)
|
|
4226
|
+
&& (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId));
|
|
4227
|
+
if (geocoder && gpt) return `готово: ${enabled.join(", ") || "geocoder"}`;
|
|
4228
|
+
if (geocoder) return "частично: geocoder";
|
|
4229
|
+
return "не настроено";
|
|
4230
|
+
}
|
|
4231
|
+
|
|
3670
4232
|
async function updateYandexEnabledServices(rawServices, enabled) {
|
|
3671
4233
|
const config = await loadConfig();
|
|
3672
4234
|
const current = new Set(config.yandex?.enabledServices || []);
|
|
@@ -4164,6 +4726,7 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4164
4726
|
if (tool === "yandex_mail_city_context") return yandexMailCityContext(args.uid || args.id, args);
|
|
4165
4727
|
if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
|
|
4166
4728
|
if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
|
|
4729
|
+
if (tool === "yandex_mail_meeting_pack") return yandexMailMeetingPack(args.uid || args.id, args);
|
|
4167
4730
|
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
4168
4731
|
if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
|
|
4169
4732
|
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
@@ -4215,6 +4778,23 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4215
4778
|
if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
|
|
4216
4779
|
if (tool === "yandex_telemost_status") return yandexTelemostStatus();
|
|
4217
4780
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
4781
|
+
if (tool === "yandex_contact_full_pack") return yandexContactFullPack(args);
|
|
4782
|
+
if (tool === "yandex_daily_digest") return yandexDailyDigestTick({ ...args, force: true });
|
|
4783
|
+
if (tool === "yandex_calendar_reminders_tick") return yandexCalendarRemindersTick({ ...args, force: true });
|
|
4784
|
+
if (tool === "yandex_disk_maintenance_tick") return yandexDiskMaintenanceTick({ ...args, force: true });
|
|
4785
|
+
if (tool === "yandex_cloud_status") {
|
|
4786
|
+
const [secrets, config] = await Promise.all([loadSecrets(), loadConfig()]);
|
|
4787
|
+
return {
|
|
4788
|
+
geocoder: Boolean(process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey),
|
|
4789
|
+
yandexgpt: Boolean((process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey)
|
|
4790
|
+
&& (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId)),
|
|
4791
|
+
enabled: config.yandex?.cloudConnector?.enabledServices || [],
|
|
4792
|
+
};
|
|
4793
|
+
}
|
|
4794
|
+
if (tool === "yandex_go_deeplink") {
|
|
4795
|
+
await ensureYandexGoGeocoderReady();
|
|
4796
|
+
return buildYandexGoDeeplinkFromOptions({ from: args.from, to: args.to, tariff: args.tariff || args.class || args.level, ref: args.ref, lang: args.lang });
|
|
4797
|
+
}
|
|
4218
4798
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
4219
4799
|
}
|
|
4220
4800
|
|
|
@@ -4385,6 +4965,142 @@ async function yandexMailWatchDisable() {
|
|
|
4385
4965
|
return { enabled: false };
|
|
4386
4966
|
}
|
|
4387
4967
|
|
|
4968
|
+
async function yandexDailyDigestEnable(options = {}) {
|
|
4969
|
+
const config = await loadConfig();
|
|
4970
|
+
const time = normalizeDigestTime(options.time || "09:00");
|
|
4971
|
+
await saveConfig({
|
|
4972
|
+
yandex: {
|
|
4973
|
+
...(config.yandex || {}),
|
|
4974
|
+
dailyDigest: { ...(config.yandex?.dailyDigest || {}), enabled: true, time, email: Boolean(options.email), save: options.save !== false, updatedAt: new Date().toISOString() },
|
|
4975
|
+
},
|
|
4976
|
+
});
|
|
4977
|
+
await upsertCronJob(`каждый день ${time}`, "yandex daily-digest tick", { replaceCommand: true });
|
|
4978
|
+
return { enabled: true, time, email: Boolean(options.email), save: options.save !== false };
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
async function yandexDailyDigestDisable() {
|
|
4982
|
+
const config = await loadConfig();
|
|
4983
|
+
const current = config.yandex?.dailyDigest || {};
|
|
4984
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
|
|
4985
|
+
deleteCronJobsByCommand("yandex daily-digest tick");
|
|
4986
|
+
return { enabled: false };
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
async function yandexDailyDigestTick(options = {}) {
|
|
4990
|
+
const config = await loadConfig();
|
|
4991
|
+
const digest = config.yandex?.dailyDigest || {};
|
|
4992
|
+
if (!digest.enabled && !options.force) return { enabled: false, text: "" };
|
|
4993
|
+
const unread = await yandexMailList({ mailbox: "INBOX", limit: 10, unread: true }).catch(() => []);
|
|
4994
|
+
const events = await yandexCalendarList({ start: new Date().toISOString(), end: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), limit: 20 }).catch(() => []);
|
|
4995
|
+
const incomplete = await yandexContactsFindIncomplete({ limit: 10 }).catch(() => []);
|
|
4996
|
+
const text = [
|
|
4997
|
+
`# Дайджест IOLA за ${new Intl.DateTimeFormat("ru-RU", { dateStyle: "long" }).format(new Date())}`,
|
|
4998
|
+
"",
|
|
4999
|
+
`Непрочитанных писем: ${unread.length}`,
|
|
5000
|
+
...unread.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`),
|
|
5001
|
+
"",
|
|
5002
|
+
`Событий на 24 часа: ${events.length}`,
|
|
5003
|
+
...events.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}`),
|
|
5004
|
+
"",
|
|
5005
|
+
`Неполных контактов: ${incomplete.length}`,
|
|
5006
|
+
...incomplete.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexContact(row)}`),
|
|
5007
|
+
].join("\n").trim();
|
|
5008
|
+
let remote = "";
|
|
5009
|
+
if (options.save !== false && digest.save !== false) {
|
|
5010
|
+
const saved = await yandexDocsCreateText({ title: `daily-digest-${timestampForFile()}`, text, format: "md", confirm: true });
|
|
5011
|
+
remote = saved.remote || "";
|
|
5012
|
+
}
|
|
5013
|
+
if (options.email || digest.email) {
|
|
5014
|
+
const profile = await getYandexIdentityProfile();
|
|
5015
|
+
const to = profile.defaultEmail || profile.emails?.[0];
|
|
5016
|
+
if (to) await yandexMailSend({ to: [to], subject: "Ежедневный дайджест IOLA", text, confirm: true });
|
|
5017
|
+
}
|
|
5018
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...digest, enabled: digest.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: remote || digest.lastRemote || "" } } });
|
|
5019
|
+
return { enabled: true, text, remote, unread: unread.length, events: events.length, incomplete: incomplete.length };
|
|
5020
|
+
}
|
|
5021
|
+
|
|
5022
|
+
async function yandexCalendarRemindersEnable(minutes = 15) {
|
|
5023
|
+
const config = await loadConfig();
|
|
5024
|
+
const safeMinutes = Math.max(1, Number(minutes || 15));
|
|
5025
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...(config.yandex?.calendarReminders || {}), enabled: true, minutes: safeMinutes, horizonMinutes: 60, updatedAt: new Date().toISOString() } } });
|
|
5026
|
+
await upsertCronJob(`каждые ${safeMinutes} минут`, "yandex calendar-reminders tick", { replaceCommand: true });
|
|
5027
|
+
return { enabled: true, minutes: safeMinutes };
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
async function yandexCalendarRemindersDisable() {
|
|
5031
|
+
const config = await loadConfig();
|
|
5032
|
+
const current = config.yandex?.calendarReminders || {};
|
|
5033
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...current, enabled: false, seen: [], updatedAt: new Date().toISOString() } } });
|
|
5034
|
+
deleteCronJobsByCommand("yandex calendar-reminders tick");
|
|
5035
|
+
return { enabled: false };
|
|
5036
|
+
}
|
|
5037
|
+
|
|
5038
|
+
async function yandexCalendarRemindersTick(options = {}) {
|
|
5039
|
+
const config = await loadConfig();
|
|
5040
|
+
const reminders = config.yandex?.calendarReminders || {};
|
|
5041
|
+
if (!reminders.enabled && !options.force) return { enabled: false, events: [] };
|
|
5042
|
+
const horizon = Math.max(5, Number(options.horizonMinutes || reminders.horizonMinutes || 60));
|
|
5043
|
+
const now = new Date();
|
|
5044
|
+
const rows = await yandexCalendarList({ start: now.toISOString(), end: new Date(now.getTime() + horizon * 60 * 1000).toISOString(), limit: 30 });
|
|
5045
|
+
const seen = new Set(reminders.seen || []);
|
|
5046
|
+
const fresh = rows.filter((row) => {
|
|
5047
|
+
const key = `${row.uid}:${row.start}`;
|
|
5048
|
+
if (seen.has(key)) return false;
|
|
5049
|
+
seen.add(key);
|
|
5050
|
+
return true;
|
|
5051
|
+
});
|
|
5052
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...reminders, enabled: reminders.enabled !== false, lastRunAt: new Date().toISOString(), lastCount: fresh.length, seen: [...seen].slice(-500) } } });
|
|
5053
|
+
return { enabled: true, events: fresh };
|
|
5054
|
+
}
|
|
5055
|
+
|
|
5056
|
+
async function yandexDiskMaintenanceEnable(days = 7) {
|
|
5057
|
+
const config = await loadConfig();
|
|
5058
|
+
const safeDays = Math.max(1, Number(days || 7));
|
|
5059
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...(config.yandex?.diskMaintenance || {}), enabled: true, days: safeDays, updatedAt: new Date().toISOString() } } });
|
|
5060
|
+
await upsertCronJob(`каждые ${safeDays} дней`, "yandex disk-maintenance tick", { replaceCommand: true });
|
|
5061
|
+
return { enabled: true, days: safeDays };
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
async function yandexDiskMaintenanceDisable() {
|
|
5065
|
+
const config = await loadConfig();
|
|
5066
|
+
const current = config.yandex?.diskMaintenance || {};
|
|
5067
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
|
|
5068
|
+
deleteCronJobsByCommand("yandex disk-maintenance tick");
|
|
5069
|
+
return { enabled: false };
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
async function yandexDiskMaintenanceTick(options = {}) {
|
|
5073
|
+
const config = await loadConfig();
|
|
5074
|
+
const disk = config.yandex?.diskMaintenance || {};
|
|
5075
|
+
if (!disk.enabled && !options.force) return { enabled: false };
|
|
5076
|
+
const info = await yandexDiskInfo();
|
|
5077
|
+
const docs = await yandexDocsList({ limit: 500 }).catch(() => []);
|
|
5078
|
+
const all = await yandexDiskListRecursive(CLOUD_DEFAULT_REMOTE_DIR, { depth: 4, limit: 500 }).catch(() => []);
|
|
5079
|
+
const publicLinks = [];
|
|
5080
|
+
for (const row of all.slice(0, 100)) {
|
|
5081
|
+
const statRow = await yandexDiskStat(row.path).catch(() => null);
|
|
5082
|
+
if (statRow?.publicUrl) publicLinks.push(statRow);
|
|
5083
|
+
}
|
|
5084
|
+
const text = [
|
|
5085
|
+
`# Проверка Яндекс Диска ${new Date().toISOString()}`,
|
|
5086
|
+
"",
|
|
5087
|
+
`Занято: ${formatBytes(info.usedSpace)} из ${formatBytes(info.totalSpace)}`,
|
|
5088
|
+
`Корзина: ${formatBytes(info.trashSize)}`,
|
|
5089
|
+
`Документов: ${docs.length}`,
|
|
5090
|
+
`Публичных ссылок в /IOLA: ${publicLinks.length}`,
|
|
5091
|
+
...publicLinks.slice(0, 20).map((row, index) => `${index + 1}. ${row.path} — ${row.publicUrl}`),
|
|
5092
|
+
].join("\n");
|
|
5093
|
+
const saved = await yandexDocsCreateText({ title: `disk-maintenance-${timestampForFile()}`, text, format: "md", confirm: true });
|
|
5094
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...disk, enabled: disk.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: saved.remote || "", lastDocs: docs.length, lastPublicLinks: publicLinks.length } } });
|
|
5095
|
+
return { enabled: true, ...info, docs: docs.length, publicLinks: publicLinks.length, remote: saved.remote || "" };
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
function normalizeDigestTime(value) {
|
|
5099
|
+
const match = String(value || "").match(/^(\d{1,2})(?::(\d{2}))?$/u);
|
|
5100
|
+
if (!match) return "09:00";
|
|
5101
|
+
return `${String(Math.min(23, Number(match[1]))).padStart(2, "0")}:${String(Math.min(59, Number(match[2] || 0))).padStart(2, "0")}`;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
4388
5104
|
async function yandexContactsMaintenanceEnable(days = 7, options = {}) {
|
|
4389
5105
|
const config = await loadConfig();
|
|
4390
5106
|
const safeDays = Math.max(1, Number(days || 7));
|
|
@@ -4588,6 +5304,47 @@ async function yandexMailCreateCalendarEvent(uid, args = {}) {
|
|
|
4588
5304
|
});
|
|
4589
5305
|
}
|
|
4590
5306
|
|
|
5307
|
+
async function yandexMailMeetingPack(uid, args = {}) {
|
|
5308
|
+
if (!args.confirm) throw new Error("Для пакетного сценария по письму нужен аргумент confirm=true.");
|
|
5309
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
5310
|
+
const mailbox = await resolveYandexMailbox(args.mailbox || args.folder || "INBOX");
|
|
5311
|
+
const row = await yandexMailRead(uid, { mailbox, markSeen: true });
|
|
5312
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
5313
|
+
const senderEmail = extractEmailAddress(row.from);
|
|
5314
|
+
const saved = await yandexMailSaveToDisk(uid, { mailbox });
|
|
5315
|
+
const shared = await yandexDiskShareWithQr(saved.remote, { confirm: true });
|
|
5316
|
+
const detected = extractDateTimeFromText(`${args.text || ""}\n${row.subject}\n${row.snippet}`);
|
|
5317
|
+
const start = args.start || detected.start || new Date(Date.now() + 3600000).toISOString();
|
|
5318
|
+
const end = args.end || detected.end || new Date(new Date(start).getTime() + 3600000).toISOString();
|
|
5319
|
+
const event = await yandexCalendarCreateEvent({
|
|
5320
|
+
title: args.title || `Встреча по письму: ${row.subject || `#${uid}`}`,
|
|
5321
|
+
description: [
|
|
5322
|
+
`Создано из письма #${uid}.`,
|
|
5323
|
+
`От: ${row.from || "-"}`,
|
|
5324
|
+
`Письмо сохранено: ${saved.remote}`,
|
|
5325
|
+
`Ссылка: ${shared.publicUrl}`,
|
|
5326
|
+
`QR-код: ${shared.qrPublicUrl}`,
|
|
5327
|
+
"",
|
|
5328
|
+
row.snippet || "",
|
|
5329
|
+
].join("\n"),
|
|
5330
|
+
location: args.location || detected.location || "",
|
|
5331
|
+
start,
|
|
5332
|
+
end,
|
|
5333
|
+
attendees: senderEmail ? [senderEmail] : [],
|
|
5334
|
+
confirm: true,
|
|
5335
|
+
});
|
|
5336
|
+
let sent = null;
|
|
5337
|
+
if ((args.send || args.email) && senderEmail) {
|
|
5338
|
+
sent = await yandexMailSend({
|
|
5339
|
+
to: [senderEmail],
|
|
5340
|
+
subject: args.subject || `Материалы к встрече: ${row.subject || `письмо #${uid}`}`,
|
|
5341
|
+
text: [`Создал встречу и сохранил письмо на Яндекс Диск.`, `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
|
|
5342
|
+
confirm: true,
|
|
5343
|
+
});
|
|
5344
|
+
}
|
|
5345
|
+
return { status: "mail-meeting-pack-created", uid: Number(uid), from: row.from, saved: saved.remote, publicUrl: shared.publicUrl, qrPublicUrl: shared.qrPublicUrl, event: event.uid, sentTo: sent?.to || [] };
|
|
5346
|
+
}
|
|
5347
|
+
|
|
4591
5348
|
async function yandexMailSenderToContact(uid, args = {}) {
|
|
4592
5349
|
if (!args.confirm) throw new Error("Для добавления отправителя в контакты нужен аргумент confirm=true.");
|
|
4593
5350
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
@@ -5645,6 +6402,47 @@ async function yandexContactFromPublicEntity(args = {}) {
|
|
|
5645
6402
|
});
|
|
5646
6403
|
}
|
|
5647
6404
|
|
|
6405
|
+
async function yandexContactFullPack(args = {}) {
|
|
6406
|
+
if (!args.confirm) throw new Error("Для полного сценария по контакту нужен аргумент confirm=true.");
|
|
6407
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
6408
|
+
if (resolved.status !== "ok") return resolved;
|
|
6409
|
+
const contact = resolved.contact;
|
|
6410
|
+
const folder = await yandexContactCreateDiskFolder({ query: contact.email || contact.name || contact.phone, confirm: true, selectFirst: true });
|
|
6411
|
+
const noteText = [
|
|
6412
|
+
`# ${contact.name || contact.email || contact.phone}`,
|
|
6413
|
+
"",
|
|
6414
|
+
`Email: ${contact.email || "-"}`,
|
|
6415
|
+
`Телефон: ${contact.phone || "-"}`,
|
|
6416
|
+
`Адрес: ${contact.address || "-"}`,
|
|
6417
|
+
`Организация: ${contact.org || "-"}`,
|
|
6418
|
+
"",
|
|
6419
|
+
args.note || args.text || "Пакет контакта создан IOLA CLI.",
|
|
6420
|
+
].join("\n");
|
|
6421
|
+
const doc = await yandexDocsCreateText({ path: path.posix.join(folder.remote, "meeting-note.md"), text: noteText, confirm: true });
|
|
6422
|
+
const shared = await yandexDiskShareWithQr(folder.remote, { confirm: true });
|
|
6423
|
+
const dateTime = args.start || args.date ? { start: args.start || args.date, end: args.end } : extractDateTimeFromText(args.text || args.source_question || "");
|
|
6424
|
+
let event = null;
|
|
6425
|
+
if (args.createEvent !== false && contact.email) {
|
|
6426
|
+
event = await yandexCalendarCreateEvent({
|
|
6427
|
+
...dateTime,
|
|
6428
|
+
title: args.title || `Встреча: ${contact.name || contact.email}`,
|
|
6429
|
+
description: [`Папка контакта: ${folder.remote}`, `Ссылка: ${shared.publicUrl}`, args.note || ""].filter(Boolean).join("\n"),
|
|
6430
|
+
attendees: [contact.email],
|
|
6431
|
+
confirm: true,
|
|
6432
|
+
});
|
|
6433
|
+
}
|
|
6434
|
+
let sent = null;
|
|
6435
|
+
if ((args.send || args.email) && contact.email) {
|
|
6436
|
+
sent = await yandexMailSend({
|
|
6437
|
+
to: [contact.email],
|
|
6438
|
+
subject: args.subject || `Материалы IOLA: ${contact.name || contact.email}`,
|
|
6439
|
+
text: [args.message || "Подготовил материалы.", `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
|
|
6440
|
+
confirm: true,
|
|
6441
|
+
});
|
|
6442
|
+
}
|
|
6443
|
+
return { status: "contact-full-pack-created", contact: contact.name || contact.email, folder: folder.remote, doc: doc.remote, publicUrl: shared.publicUrl, qrPublicUrl: shared.qrPublicUrl, event: event?.uid || "", sentTo: sent?.to || [] };
|
|
6444
|
+
}
|
|
6445
|
+
|
|
5648
6446
|
async function resolveYandexContact(query, args = {}) {
|
|
5649
6447
|
const rows = await yandexContactsList({ limit: Math.max(500, Number(args.limit || 100)), full: true });
|
|
5650
6448
|
const normalized = normalizeContactLookupText(query || args.query || args.name || args.email || args.phone || "");
|
|
@@ -9147,6 +9945,17 @@ async function chooseOpenRouterModel() {
|
|
|
9147
9945
|
async function ensureApiKeyForModelSelection(provider) {
|
|
9148
9946
|
if (!["openai", "openrouter", "yandexgpt", "gigachat"].includes(provider)) return true;
|
|
9149
9947
|
if (await getApiKey(provider) && (provider !== "yandexgpt" || await getYandexFolderId())) return true;
|
|
9948
|
+
if (provider === "yandexgpt") {
|
|
9949
|
+
console.log("YandexGPT требует Yandex Cloud Connector: API key и folder ID.");
|
|
9950
|
+
if (!process.stdin.isTTY) return false;
|
|
9951
|
+
const ok = await askYesNo("Включить Yandex Cloud Connector сейчас? [y/N] ", false);
|
|
9952
|
+
if (!ok) {
|
|
9953
|
+
console.log("Возврат в меню выбора модели.");
|
|
9954
|
+
return false;
|
|
9955
|
+
}
|
|
9956
|
+
await setupYandexCloudConnector({});
|
|
9957
|
+
return Boolean(await getApiKey("yandexgpt") && await getYandexFolderId());
|
|
9958
|
+
}
|
|
9150
9959
|
const label = {
|
|
9151
9960
|
openai: "OpenAI",
|
|
9152
9961
|
openrouter: "OpenRouter",
|
|
@@ -9983,7 +10792,7 @@ async function getYandexGeocoderKey() {
|
|
|
9983
10792
|
return process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY;
|
|
9984
10793
|
}
|
|
9985
10794
|
const secrets = await loadSecrets();
|
|
9986
|
-
return secrets.yandexGeocoder?.apiKey || "";
|
|
10795
|
+
return secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey || "";
|
|
9987
10796
|
}
|
|
9988
10797
|
|
|
9989
10798
|
function openDatabase() {
|
|
@@ -11084,6 +11893,13 @@ function isCronDue(job) {
|
|
|
11084
11893
|
return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyDays[1]) * 24 * 60 * 60 * 1000;
|
|
11085
11894
|
}
|
|
11086
11895
|
if (normalized.includes("каждый день") || normalized.includes("daily")) {
|
|
11896
|
+
const time = normalized.match(/(\d{1,2}):(\d{2})/u);
|
|
11897
|
+
if (time) {
|
|
11898
|
+
const dueAt = new Date(now);
|
|
11899
|
+
dueAt.setHours(Number(time[1]), Number(time[2]), 0, 0);
|
|
11900
|
+
const ranToday = lastRun && lastRun.toISOString().slice(0, 10) === now.toISOString().slice(0, 10);
|
|
11901
|
+
return now >= dueAt && !ranToday;
|
|
11902
|
+
}
|
|
11087
11903
|
return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
|
|
11088
11904
|
}
|
|
11089
11905
|
if (normalized.includes("каждую неделю") || normalized.includes("weekly")) {
|
|
@@ -11635,6 +12451,60 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
11635
12451
|
].join("\n");
|
|
11636
12452
|
}
|
|
11637
12453
|
|
|
12454
|
+
if (/(яндекс\s*go|яндекс\s*го|такси|deeplink|диплинк|ссылк.*маршрут)/iu.test(normalized)
|
|
12455
|
+
&& /(маршрут|ссылк|откуда|куда|поездк|такси|от\s+.+\s+до\s+)/iu.test(normalized)) {
|
|
12456
|
+
const route = extractYandexGoRouteFromText(question);
|
|
12457
|
+
if (!route.from || !route.to) {
|
|
12458
|
+
return 'Для ссылки Яндекс Go нужны два адреса. Пример: "такси от Медведево, Школьная 15 до Медведево, Советская 20".';
|
|
12459
|
+
}
|
|
12460
|
+
await ensureYandexGoGeocoderReady();
|
|
12461
|
+
const result = await buildYandexGoDeeplinkFromOptions({ from: route.from, to: route.to, tariff: route.tariff });
|
|
12462
|
+
return formatYandexGoDeeplinkResult(result);
|
|
12463
|
+
}
|
|
12464
|
+
|
|
12465
|
+
if (/(дайджест|сводк)/iu.test(normalized) && /(яндекс|почт|календар|контакт|диск)/iu.test(normalized)) {
|
|
12466
|
+
if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized) && /(кажд|ежеднев|авто|регуляр)/iu.test(normalized)) {
|
|
12467
|
+
const time = question.match(/(\d{1,2}:\d{2})/u)?.[1] || "09:00";
|
|
12468
|
+
const result = await yandexDailyDigestEnable({ time, email: /email|почт[уы]|письм/iu.test(normalized), save: true });
|
|
12469
|
+
return `Ежедневный дайджест включен: каждый день ${result.time}.`;
|
|
12470
|
+
}
|
|
12471
|
+
if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
12472
|
+
await yandexDailyDigestDisable();
|
|
12473
|
+
return "Ежедневный дайджест выключен.";
|
|
12474
|
+
}
|
|
12475
|
+
const result = await yandexDailyDigestTick({ force: true, save: true, email: /отправь|email|почт[уы]/iu.test(normalized) });
|
|
12476
|
+
return [result.text, result.remote ? `\nСохранено: ${result.remote}` : ""].join("").trim();
|
|
12477
|
+
}
|
|
12478
|
+
|
|
12479
|
+
if (/(календар|событи|встреч)/iu.test(normalized) && /(напомин|уведом|следи|монитор|авто|регуляр)/iu.test(normalized)) {
|
|
12480
|
+
if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
12481
|
+
const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
|
|
12482
|
+
await yandexCalendarRemindersEnable(minutes);
|
|
12483
|
+
return `Проверка календарных напоминаний включена: каждые ${minutes} минут.`;
|
|
12484
|
+
}
|
|
12485
|
+
if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
12486
|
+
await yandexCalendarRemindersDisable();
|
|
12487
|
+
return "Проверка календарных напоминаний выключена.";
|
|
12488
|
+
}
|
|
12489
|
+
const result = await yandexCalendarRemindersTick({ force: true });
|
|
12490
|
+
if (!result.events.length) return "Ближайших событий для напоминания нет.";
|
|
12491
|
+
return ["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n");
|
|
12492
|
+
}
|
|
12493
|
+
|
|
12494
|
+
if (/(диск|яндекс.?диск|документ)/iu.test(normalized) && /(провер|обслуж|maintenance|аудит|регуляр|авто)/iu.test(normalized)) {
|
|
12495
|
+
if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
12496
|
+
const days = Number(question.match(/(\d+)\s*(?:дн|день|дня|дней)/iu)?.[1] || 7);
|
|
12497
|
+
await yandexDiskMaintenanceEnable(days);
|
|
12498
|
+
return `Проверка Яндекс Диска включена: каждые ${days} дней.`;
|
|
12499
|
+
}
|
|
12500
|
+
if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
12501
|
+
await yandexDiskMaintenanceDisable();
|
|
12502
|
+
return "Проверка Яндекс Диска выключена.";
|
|
12503
|
+
}
|
|
12504
|
+
const result = await yandexDiskMaintenanceTick({ force: true });
|
|
12505
|
+
return `Проверка Яндекс Диска выполнена. Документов: ${result.docs}, публичных ссылок: ${result.publicLinks}. Отчет: ${result.remote}.`;
|
|
12506
|
+
}
|
|
12507
|
+
|
|
11638
12508
|
if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
|
|
11639
12509
|
return await buildYandexContactsDirectAnswer(question, normalized);
|
|
11640
12510
|
}
|
|
@@ -11682,6 +12552,12 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
11682
12552
|
const result = await yandexMailSaveToDisk(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
|
|
11683
12553
|
return `Письмо #${uid} сохранено на Яндекс Диск: ${result.remote || result.path}.`;
|
|
11684
12554
|
}
|
|
12555
|
+
if (/(пакет|комплект|встреч).{0,80}(письм|письма)|(?:письм|письма).{0,80}(пакет|комплект|встреч)/iu.test(normalized) && /(диск|ссылк|qr|календар|встреч)/iu.test(normalized)) {
|
|
12556
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
12557
|
+
if (!uid) return "Из какого письма создать пакет? Укажите номер из списка или UID.";
|
|
12558
|
+
const result = await yandexMailMeetingPack(uid, { mailbox: extractYandexMailboxName(question) || "INBOX", ...extractDateTimeFromText(question), confirm: true });
|
|
12559
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12560
|
+
}
|
|
11685
12561
|
if (/(создай|добавь).{0,40}(событи|встреч|календар)/iu.test(normalized)) {
|
|
11686
12562
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
11687
12563
|
if (!uid) return "Из какого письма создать событие? Укажите номер из списка или UID.";
|
|
@@ -11983,7 +12859,9 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
11983
12859
|
return ["Яндекс Контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
|
|
11984
12860
|
}
|
|
11985
12861
|
} catch (error) {
|
|
11986
|
-
|
|
12862
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12863
|
+
if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT нужны)/u.test(message)) return message;
|
|
12864
|
+
return `Не смог выполнить запрос к сервисам Яндекса: ${message}`;
|
|
11987
12865
|
}
|
|
11988
12866
|
return "";
|
|
11989
12867
|
}
|
|
@@ -12122,6 +13000,11 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
|
|
|
12122
13000
|
const result = await yandexContactCreateDiskFolder({ query, confirm: true });
|
|
12123
13001
|
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12124
13002
|
}
|
|
13003
|
+
if (/(полный|комплект|пакет|подготовь).{0,80}(контакт|клиент|человек)/iu.test(text) || /(контакт|клиент|человек).{0,80}(полный|комплект|пакет)/iu.test(text)) {
|
|
13004
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
13005
|
+
const result = await yandexContactFullPack({ query, ...extractDateTimeFromText(question), note: extractShareMessage(question), confirm: true });
|
|
13006
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
13007
|
+
}
|
|
12125
13008
|
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(text)) {
|
|
12126
13009
|
const remotePath = extractCloudPath(question);
|
|
12127
13010
|
const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
|
|
@@ -12156,6 +13039,27 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
|
|
|
12156
13039
|
async function buildUserSkillDirectAnswer(question) {
|
|
12157
13040
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
12158
13041
|
if (!/(skill|скилл|скил|навык)/iu.test(normalized)) return "";
|
|
13042
|
+
if (/(шаблон|template|вариант)/iu.test(normalized) && /(покажи|список|какие|list)/iu.test(normalized)) {
|
|
13043
|
+
const rows = userSkillTemplates();
|
|
13044
|
+
return ["Шаблоны skills:", ...rows.map((row) => `- ${row.name}: ${row.description}`)].join("\n");
|
|
13045
|
+
}
|
|
13046
|
+
if (/(preview|предпросмотр|покажи\s+как)/iu.test(normalized)) {
|
|
13047
|
+
const name = extractUserSkillNameFromQuestion(question) || "user-skill";
|
|
13048
|
+
const template = normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || "");
|
|
13049
|
+
return buildUserSkillPreview({ name, template, instructions: question });
|
|
13050
|
+
}
|
|
13051
|
+
if (/(проверь|validate|doctor|валидац)/iu.test(normalized)) {
|
|
13052
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
13053
|
+
if (!name) return "Какой skill проверить? Укажите имя.";
|
|
13054
|
+
const result = await userSkillValidate(name);
|
|
13055
|
+
return ["Проверка skill:", ...result.checks.map((row) => `${row.status}: ${row.check} - ${row.message}`)].join("\n");
|
|
13056
|
+
}
|
|
13057
|
+
if (/(обнови|измени|update|edit)/iu.test(normalized)) {
|
|
13058
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
13059
|
+
if (!name) return "Какой skill обновить? Укажите имя.";
|
|
13060
|
+
const result = await userSkillUpdate(name, { instructions: question, tools: inferUserSkillTools(question), confirm: true });
|
|
13061
|
+
return `Skill обновлен: ${result.name}\nФайл: ${result.file}`;
|
|
13062
|
+
}
|
|
12159
13063
|
if (/(создай|добавь|сделай|create|new)/iu.test(normalized)) {
|
|
12160
13064
|
const name = extractUserSkillNameFromQuestion(question) || "user-skill";
|
|
12161
13065
|
const result = await userSkillCreate({
|
|
@@ -12163,6 +13067,7 @@ async function buildUserSkillDirectAnswer(question) {
|
|
|
12163
13067
|
description: extractUserSkillDescription(question, name),
|
|
12164
13068
|
instructions: question,
|
|
12165
13069
|
tools: inferUserSkillTools(question),
|
|
13070
|
+
template: normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || ""),
|
|
12166
13071
|
enable: true,
|
|
12167
13072
|
confirm: true,
|
|
12168
13073
|
});
|
|
@@ -12195,7 +13100,7 @@ async function buildUserSkillDirectAnswer(question) {
|
|
|
12195
13100
|
}
|
|
12196
13101
|
|
|
12197
13102
|
function isYandexServiceQuestion(normalized) {
|
|
12198
|
-
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360
|
|
13103
|
+
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин|такси|яндекс\s*go|яндекс\s*го|геокод|cloud|клауд)/iu.test(String(normalized || ""));
|
|
12199
13104
|
}
|
|
12200
13105
|
|
|
12201
13106
|
function isYandexIdentityQuestion(normalized) {
|
|
@@ -13415,9 +14320,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
13415
14320
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
13416
14321
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
13417
14322
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
13418
|
-
"Yandex tools: yandex_identity_me {}, yandex_disk_info {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_stat {path}, yandex_disk_exists {path}, yandex_disk_read_text {path}, yandex_disk_save_text {path,text}, yandex_disk_upload {localPath,remotePath}, yandex_disk_download {remotePath,outputPath}, yandex_disk_move {from,to,confirm}, yandex_disk_copy {from,to,confirm}, yandex_disk_rename {path,name,confirm}, yandex_disk_share {path,confirm}, yandex_disk_share_qr {path,confirm}, yandex_disk_share_email {path,to,contact,subject,text,confirm}, yandex_disk_package_share_email {sourcePath,targetFolder,to,contact,mode,confirm}, yandex_disk_unshare {path}, yandex_disk_delete {path,confirm}, yandex_disk_trash_list {}, yandex_disk_restore {path,confirm}, yandex_disk_empty_trash {confirm}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_send {to,subject,text,confirm}, yandex_mail_reply {uid,text,confirm}, yandex_mail_forward {uid,to,confirm}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_calendar_calendars {}, yandex_calendar_list {start,end}, yandex_calendar_search {query,start,end}, yandex_calendar_get {query}, yandex_calendar_create_event {title,start,end,location,attendees,reminders,confirm}, yandex_calendar_update {query,title,start,end,location,description,reminders,confirm}, yandex_calendar_move {query,start,end,confirm}, yandex_calendar_delete {query,confirm}, yandex_docs_list {path}, yandex_docs_find {query}, yandex_docs_create_text {title,text,format,confirm}, yandex_docs_read {path|query}, yandex_docs_share {path|query,confirm}, yandex_docs_rename {path|query,name,confirm}, yandex_docs_delete {path|query,confirm}, yandex_contacts_list {limit}, yandex_contacts_search {query}, yandex_contacts_get {query}, yandex_contacts_create {name,email,phone,address,note,confirm}, yandex_contacts_update {query,email,phone,address,note,birthday,org,title,confirm}, yandex_contacts_delete {query,confirm}, yandex_contacts_export_csv {}, yandex_contacts_find_incomplete {}, yandex_contacts_find_duplicates {}, yandex_contacts_backup_to_disk {format,confirm}, yandex_contact_send_mail {contact,subject,text,confirm}, yandex_contact_send_disk_link_qr {contact,path,confirm}, yandex_contact_create_disk_folder {contact,confirm}, yandex_contact_create_calendar_event {contact,start,end,title,confirm}, yandex_contact_create_telemost_event {contact,start,end,title,confirm}.",
|
|
13419
|
-
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_calendar_create_event, yandex_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event.",
|
|
13420
|
-
"User skill tools: user_skill_create {name,description,instructions,tools,enable,confirm}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай skill только по явной просьбе пользователя и с confirm=true.",
|
|
14323
|
+
"Yandex tools: yandex_identity_me {}, yandex_disk_info {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_stat {path}, yandex_disk_exists {path}, yandex_disk_read_text {path}, yandex_disk_save_text {path,text}, yandex_disk_upload {localPath,remotePath}, yandex_disk_download {remotePath,outputPath}, yandex_disk_move {from,to,confirm}, yandex_disk_copy {from,to,confirm}, yandex_disk_rename {path,name,confirm}, yandex_disk_share {path,confirm}, yandex_disk_share_qr {path,confirm}, yandex_disk_share_email {path,to,contact,subject,text,confirm}, yandex_disk_package_share_email {sourcePath,targetFolder,to,contact,mode,confirm}, yandex_disk_unshare {path}, yandex_disk_delete {path,confirm}, yandex_disk_trash_list {}, yandex_disk_restore {path,confirm}, yandex_disk_empty_trash {confirm}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_send {to,subject,text,confirm}, yandex_mail_reply {uid,text,confirm}, yandex_mail_forward {uid,to,confirm}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_mail_meeting_pack {uid,start,end,send,confirm}, yandex_calendar_calendars {}, yandex_calendar_list {start,end}, yandex_calendar_search {query,start,end}, yandex_calendar_get {query}, yandex_calendar_create_event {title,start,end,location,attendees,reminders,confirm}, yandex_calendar_update {query,title,start,end,location,description,reminders,confirm}, yandex_calendar_move {query,start,end,confirm}, yandex_calendar_delete {query,confirm}, yandex_docs_list {path}, yandex_docs_find {query}, yandex_docs_create_text {title,text,format,confirm}, yandex_docs_read {path|query}, yandex_docs_share {path|query,confirm}, yandex_docs_rename {path|query,name,confirm}, yandex_docs_delete {path|query,confirm}, yandex_contacts_list {limit}, yandex_contacts_search {query}, yandex_contacts_get {query}, yandex_contacts_create {name,email,phone,address,note,confirm}, yandex_contacts_update {query,email,phone,address,note,birthday,org,title,confirm}, yandex_contacts_delete {query,confirm}, yandex_contacts_export_csv {}, yandex_contacts_find_incomplete {}, yandex_contacts_find_duplicates {}, yandex_contacts_backup_to_disk {format,confirm}, yandex_contact_send_mail {contact,subject,text,confirm}, yandex_contact_send_disk_link_qr {contact,path,confirm}, yandex_contact_create_disk_folder {contact,confirm}, yandex_contact_create_calendar_event {contact,start,end,title,confirm}, yandex_contact_create_telemost_event {contact,start,end,title,confirm}, yandex_contact_full_pack {contact,start,end,send,confirm}, yandex_cloud_status {}, yandex_go_deeplink {from,to,tariff}, yandex_daily_digest {save,email}, yandex_calendar_reminders_tick {}, yandex_disk_maintenance_tick {}.",
|
|
14324
|
+
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_mail_meeting_pack, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_contact_full_pack, yandex_calendar_create_event, yandex_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event.",
|
|
14325
|
+
"User skill tools: user_skill_create {name,description,instructions,tools,template,enable,confirm}, user_skill_update {name,instructions,tools,confirm}, user_skill_templates {}, user_skill_validate {name}, user_skill_preview {name,template,instructions}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай или меняй skill только по явной просьбе пользователя и с confirm=true.",
|
|
13421
14326
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
13422
14327
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
13423
14328
|
`Вопрос: ${question}`,
|
|
@@ -13497,6 +14402,12 @@ function inferToolPlan(question, options = {}) {
|
|
|
13497
14402
|
}],
|
|
13498
14403
|
};
|
|
13499
14404
|
}
|
|
14405
|
+
if (/(шаблон|template)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
|
|
14406
|
+
return { steps: [{ tool: "user_skill_templates", args: {} }] };
|
|
14407
|
+
}
|
|
14408
|
+
if (/(проверь|validate|doctor)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
|
|
14409
|
+
return { steps: [{ tool: "user_skill_validate", args: { name: extractUserSkillNameFromQuestion(question) } }] };
|
|
14410
|
+
}
|
|
13500
14411
|
if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
|
|
13501
14412
|
return { steps: [{ tool: "yandex_identity_me", args: {} }] };
|
|
13502
14413
|
}
|
|
@@ -13527,6 +14438,7 @@ function inferToolPlan(question, options = {}) {
|
|
|
13527
14438
|
if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_reply", args: { uid, mailbox, text: parseYandexMailReplyRequest(question).text, confirm: true } }] };
|
|
13528
14439
|
if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_delete", args: { uid, mailbox, confirm: true } }] };
|
|
13529
14440
|
if (/(пометь|отметь|сделай)/iu.test(normalized) && /(прочитан|непрочитан)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_mark", args: { uid, mailbox, seen: !/непрочитан/iu.test(normalized) } }] };
|
|
14441
|
+
if (/(пакет|комплект)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_meeting_pack", args: { uid, mailbox, ...extractDateTimeFromText(question), confirm: true } }] };
|
|
13530
14442
|
if (/(прочитай|прочти|открой|раскрой|получи|получить)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_read", args: { uid, mailbox } }] };
|
|
13531
14443
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
|
|
13532
14444
|
return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
|
|
@@ -13542,6 +14454,7 @@ function inferToolPlan(question, options = {}) {
|
|
|
13542
14454
|
return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
|
|
13543
14455
|
}
|
|
13544
14456
|
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
14457
|
+
if (/(напомин|уведом|следи|монитор)/iu.test(normalized) && /(проверь|tick|сейчас)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_reminders_tick", args: { force: true } }] };
|
|
13545
14458
|
if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
|
|
13546
14459
|
const dateTime = extractDateTimeFromText(question);
|
|
13547
14460
|
return { steps: [{ tool: /телемост/iu.test(normalized) ? "yandex_telemost_create_event" : "yandex_calendar_create_event", args: { ...dateTime, title: extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA"), confirm: true } }] };
|
|
@@ -13557,6 +14470,7 @@ function inferToolPlan(question, options = {}) {
|
|
|
13557
14470
|
if (/(экспорт|выгруз)/iu.test(normalized)) return { steps: [{ tool: /csv/iu.test(normalized) ? "yandex_contacts_export_csv" : "yandex_contacts_export_vcard", args: {} }] };
|
|
13558
14471
|
if (/(резерв|backup|бэкап|диск|яндекс.?диск)/iu.test(normalized) && /(контакт)/iu.test(normalized) && /(сохрани|экспорт|выгруз|резерв|backup|бэкап)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_backup_to_disk", args: { format: /csv/iu.test(normalized) ? "csv" : "vcard", confirm: true } }] };
|
|
13559
14472
|
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_create", args: { ...parseYandexContactCreateRequest(question), confirm: true } }] };
|
|
14473
|
+
if (/(полный|комплект|пакет)/iu.test(normalized)) return { steps: [{ tool: "yandex_contact_full_pack", args: { contact: cleanupYandexContactActionQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
|
|
13560
14474
|
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_delete", args: { query: cleanupYandexContactActionQuery(question), confirm: true } }] };
|
|
13561
14475
|
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(normalized)) return { steps: [{ tool: "yandex_contact_send_disk_link_qr", args: { contact: cleanupYandexContactActionQuery(question), path: extractCloudPath(question), confirm: true } }] };
|
|
13562
14476
|
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
|
|
@@ -14148,6 +15062,11 @@ function formatToolResult(result, options) {
|
|
|
14148
15062
|
}
|
|
14149
15063
|
if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
|
|
14150
15064
|
if (row.status === "calendar-event-deleted") return `Событие удалено: ${row.title || row.uid}`;
|
|
15065
|
+
if (row.status === "mail-meeting-pack-created") return `Пакет по письму #${row.uid} создан.\nПисьмо: ${row.saved}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}\nСобытие: ${row.event || "-"}`;
|
|
15066
|
+
if (row.status === "contact-full-pack-created") return `Пакет контакта создан: ${row.contact}\nПапка: ${row.folder}\nДокумент: ${row.doc}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}\nСобытие: ${row.event || "-"}`;
|
|
15067
|
+
if (row.url && row.fromPoint && row.toPoint) return formatYandexGoDeeplinkResult(row);
|
|
15068
|
+
if (row.geocoder !== undefined && row.yandexgpt !== undefined && row.enabled) return `Yandex Cloud Connector:\nГеокодер: ${row.geocoder ? "настроен" : "нет"}\nYandexGPT: ${row.yandexgpt ? "настроен" : "нет"}\nВключено: ${row.enabled.join(", ") || "-"}`;
|
|
15069
|
+
if (row.enabled && row.text && (row.unread !== undefined || row.events !== undefined)) return row.text;
|
|
14151
15070
|
if (row.status === "ambiguous" && row.events) return [`Нашел несколько событий. Уточните:`, ...row.events.map((event, index) => `${index + 1}. ${event.title || event.uid} — ${event.startIso || event.start || "-"}`)].join("\n");
|
|
14152
15071
|
if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
|
|
14153
15072
|
if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
|
|
@@ -15277,6 +16196,7 @@ async function getApiKey(provider) {
|
|
|
15277
16196
|
}
|
|
15278
16197
|
|
|
15279
16198
|
const secrets = await loadSecrets();
|
|
16199
|
+
if (provider === "yandexgpt") return secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey || "";
|
|
15280
16200
|
return secrets[provider]?.apiKey || "";
|
|
15281
16201
|
}
|
|
15282
16202
|
|
|
@@ -15285,7 +16205,7 @@ async function getYandexFolderId() {
|
|
|
15285
16205
|
return process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID;
|
|
15286
16206
|
}
|
|
15287
16207
|
const secrets = await loadSecrets();
|
|
15288
|
-
return secrets.yandexgpt?.folderId || "";
|
|
16208
|
+
return secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId || "";
|
|
15289
16209
|
}
|
|
15290
16210
|
|
|
15291
16211
|
async function listLayers(args) {
|
|
@@ -15545,12 +16465,8 @@ async function onboard(args = []) {
|
|
|
15545
16465
|
await chooseAndSaveApiModel("openrouter");
|
|
15546
16466
|
}
|
|
15547
16467
|
}
|
|
15548
|
-
if (components.includes("
|
|
15549
|
-
await
|
|
15550
|
-
if (process.stdin.isTTY) {
|
|
15551
|
-
await setAiKey("yandexgpt");
|
|
15552
|
-
await chooseAndSaveApiModel("yandexgpt");
|
|
15553
|
-
}
|
|
16468
|
+
if (components.includes("yandex-cloud")) {
|
|
16469
|
+
await setupYandexCloudConnector({});
|
|
15554
16470
|
}
|
|
15555
16471
|
if (components.includes("gigachat")) {
|
|
15556
16472
|
await aiSetup(["gigachat"]);
|
|
@@ -15559,11 +16475,6 @@ async function onboard(args = []) {
|
|
|
15559
16475
|
await chooseAndSaveApiModel("gigachat");
|
|
15560
16476
|
}
|
|
15561
16477
|
}
|
|
15562
|
-
if (components.includes("yandex-geocoder")) {
|
|
15563
|
-
if (process.stdin.isTTY) {
|
|
15564
|
-
await setYandexGeocoderKey();
|
|
15565
|
-
}
|
|
15566
|
-
}
|
|
15567
16478
|
if (components.includes("cloud")) {
|
|
15568
16479
|
if (process.stdin.isTTY) {
|
|
15569
16480
|
const providerAnswer = (await askText("Облачный диск: 1 - Яндекс Диск, 2 - Облако Mail.ru, 0 - пропустить: ")).trim();
|
|
@@ -15613,7 +16524,7 @@ async function chooseOnboardComponents(status = null) {
|
|
|
15613
16524
|
1: "workspace",
|
|
15614
16525
|
2: "policy",
|
|
15615
16526
|
3: "iola",
|
|
15616
|
-
4: "
|
|
16527
|
+
4: "yandex-cloud",
|
|
15617
16528
|
5: "gigachat",
|
|
15618
16529
|
6: "openai",
|
|
15619
16530
|
7: "openrouter",
|
|
@@ -15623,8 +16534,8 @@ async function chooseOnboardComponents(status = null) {
|
|
|
15623
16534
|
11: "index",
|
|
15624
16535
|
12: "browser",
|
|
15625
16536
|
13: "ollama",
|
|
15626
|
-
14: "
|
|
15627
|
-
15: "
|
|
16537
|
+
14: "cloud",
|
|
16538
|
+
15: "yandex",
|
|
15628
16539
|
16: "yandex",
|
|
15629
16540
|
};
|
|
15630
16541
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
@@ -15656,7 +16567,7 @@ async function getOnboardComponentStatus() {
|
|
|
15656
16567
|
policy: policyReady,
|
|
15657
16568
|
iola: Boolean(readiness.iola),
|
|
15658
16569
|
ollama: Boolean(ollamaVersion && readiness.ollama),
|
|
15659
|
-
|
|
16570
|
+
"yandex-cloud": Boolean(yandexGeocoderKey || readiness.yandexgpt),
|
|
15660
16571
|
gigachat: Boolean(readiness.gigachat),
|
|
15661
16572
|
openai: Boolean(readiness.openai),
|
|
15662
16573
|
openrouter: Boolean(readiness.openrouter),
|
|
@@ -15665,7 +16576,6 @@ async function getOnboardComponentStatus() {
|
|
|
15665
16576
|
archive: Boolean(archive),
|
|
15666
16577
|
index: false,
|
|
15667
16578
|
browser: browser.installed === "yes",
|
|
15668
|
-
"yandex-geocoder": Boolean(yandexGeocoderKey),
|
|
15669
16579
|
cloud: Object.keys(cloudSecrets).length > 0,
|
|
15670
16580
|
yandex: isYandexConnectorFullyConnected(secrets),
|
|
15671
16581
|
};
|
|
@@ -15676,7 +16586,7 @@ function onboardComponentRows(status) {
|
|
|
15676
16586
|
["1", "workspace", "workspace и контекст", "рабочая папка, IOLA.md и .iola/context.md"],
|
|
15677
16587
|
["2", "policy", "policy analyst", "разрешения и профиль аналитика"],
|
|
15678
16588
|
["3", "iola", "IOLA локальная модель", "локальная модель найдена"],
|
|
15679
|
-
["4", "
|
|
16589
|
+
["4", "yandex-cloud", "Yandex Cloud Connector", "геокодинг и YandexGPT"],
|
|
15680
16590
|
["5", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
|
|
15681
16591
|
["6", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
|
|
15682
16592
|
["7", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
|
|
@@ -15686,9 +16596,8 @@ function onboardComponentRows(status) {
|
|
|
15686
16596
|
["11", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
|
|
15687
16597
|
["12", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
15688
16598
|
["13", "ollama", "Ollama", "опциональный локальный runtime"],
|
|
15689
|
-
["14", "
|
|
15690
|
-
["15", "
|
|
15691
|
-
["16", "yandex", "Yandex Connector", "единый вход и категории сервисов Яндекса"],
|
|
16599
|
+
["14", "cloud", "Облачный диск", "Яндекс Диск или Облако Mail.ru"],
|
|
16600
|
+
["15", "yandex", "Yandex Connector", "единый вход и категории сервисов Яндекса"],
|
|
15692
16601
|
];
|
|
15693
16602
|
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
15694
16603
|
}
|
|
@@ -15703,7 +16612,7 @@ function defaultOnboardSelection(status) {
|
|
|
15703
16612
|
}
|
|
15704
16613
|
|
|
15705
16614
|
function defaultOnboardComponents(status) {
|
|
15706
|
-
const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "
|
|
16615
|
+
const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "yandex-cloud", 5: "gigachat", 6: "openai", 7: "openrouter", 8: "codex", 9: "codex-mcp", 10: "archive", 11: "index", 12: "browser", 13: "ollama", 14: "cloud", 15: "yandex", 16: "yandex" };
|
|
15707
16616
|
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
15708
16617
|
}
|
|
15709
16618
|
|
|
@@ -15712,12 +16621,12 @@ function parseOptions(args) {
|
|
|
15712
16621
|
|
|
15713
16622
|
for (let index = 0; index < args.length; index += 1) {
|
|
15714
16623
|
const arg = args[index];
|
|
15715
|
-
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 === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url" || arg === "--enable") {
|
|
16624
|
+
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 === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url" || arg === "--enable" || arg === "--email" || arg === "--backup") {
|
|
15716
16625
|
result[arg.slice(2)] = true;
|
|
15717
16626
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
15718
16627
|
result.check = true;
|
|
15719
16628
|
result[arg.slice(2)] = true;
|
|
15720
|
-
} 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 === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || 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-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app") {
|
|
16629
|
+
} 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 === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--template" || arg === "--minutes" || arg === "--days" || arg === "--time" || arg === "--horizon" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || 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-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app" || arg === "--tariff" || arg === "--class" || arg === "--level" || arg === "--ref" || arg === "--lang") {
|
|
15721
16630
|
result[arg.slice(2)] = args[index + 1];
|
|
15722
16631
|
index += 1;
|
|
15723
16632
|
} else {
|
|
@@ -15941,9 +16850,13 @@ function printSkillsList(skills, config) {
|
|
|
15941
16850
|
async function executeUserSkillTool(tool, args = {}) {
|
|
15942
16851
|
if (tool === "user_skill_list") return listSkills(await loadConfig()).filter((skill) => skill.source === "user");
|
|
15943
16852
|
if (tool === "user_skill_create") return userSkillCreate({ ...args, confirm: args.confirm === true });
|
|
16853
|
+
if (tool === "user_skill_update") return userSkillUpdate(args.name, { ...args, confirm: args.confirm === true });
|
|
15944
16854
|
if (tool === "user_skill_enable") return userSkillSetEnabled(args.name, true);
|
|
15945
16855
|
if (tool === "user_skill_disable") return userSkillSetEnabled(args.name, false);
|
|
15946
16856
|
if (tool === "user_skill_delete") return userSkillDelete(args.name, { confirm: args.confirm === true });
|
|
16857
|
+
if (tool === "user_skill_templates") return userSkillTemplates();
|
|
16858
|
+
if (tool === "user_skill_validate") return userSkillValidate(args.name);
|
|
16859
|
+
if (tool === "user_skill_preview") return { status: "preview", text: buildUserSkillPreview(args) };
|
|
15947
16860
|
throw new Error(`Неизвестный user skill tool: ${tool}`);
|
|
15948
16861
|
}
|
|
15949
16862
|
|
|
@@ -15951,22 +16864,41 @@ async function userSkillCreate(args = {}) {
|
|
|
15951
16864
|
if (!args.confirm) throw new Error("Для создания пользовательского skill нужен аргумент confirm=true.");
|
|
15952
16865
|
const name = normalizeUserSkillName(args.name || args.skill || args.title);
|
|
15953
16866
|
if (!name) throw new Error("Имя skill обязательно.");
|
|
15954
|
-
const
|
|
15955
|
-
const
|
|
16867
|
+
const template = getUserSkillTemplate(args.template);
|
|
16868
|
+
const description = String(args.description || template?.description || `Пользовательский skill: ${name}`).trim();
|
|
16869
|
+
const instructions = String(args.instructions || args.text || args.prompt || template?.instructions || "").trim();
|
|
15956
16870
|
if (!instructions) throw new Error("Инструкции skill обязательны.");
|
|
15957
16871
|
const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || "");
|
|
16872
|
+
const mergedTools = [...new Set([...(template?.tools || []), ...tools])];
|
|
15958
16873
|
const dir = path.join(USER_SKILLS_DIR, name);
|
|
15959
16874
|
const file = path.join(dir, "SKILL.md");
|
|
15960
16875
|
if (existsSync(file) && !args.overwrite) throw new Error(`Skill уже существует: ${name}. Используйте overwrite=true или --force.`);
|
|
15961
16876
|
await mkdir(dir, { recursive: true });
|
|
15962
|
-
const body = buildUserSkillMarkdown({ name, description, instructions, tools });
|
|
16877
|
+
const body = buildUserSkillMarkdown({ name, description, instructions, tools: mergedTools });
|
|
15963
16878
|
await writeFile(file, body, "utf8");
|
|
15964
16879
|
let enabled = false;
|
|
15965
16880
|
if (args.enable) {
|
|
15966
16881
|
await userSkillSetEnabled(name, true);
|
|
15967
16882
|
enabled = true;
|
|
15968
16883
|
}
|
|
15969
|
-
return { name, description, file, enabled, tools, status: "created" };
|
|
16884
|
+
return { name, description, file, enabled, tools: mergedTools, status: "created" };
|
|
16885
|
+
}
|
|
16886
|
+
|
|
16887
|
+
async function userSkillUpdate(name, args = {}) {
|
|
16888
|
+
if (!args.confirm) throw new Error("Для обновления пользовательского skill нужен confirm=true.");
|
|
16889
|
+
const skillName = normalizeUserSkillName(name || args.name || args.skill);
|
|
16890
|
+
if (!skillName) throw new Error("Имя skill обязательно.");
|
|
16891
|
+
const file = path.join(USER_SKILLS_DIR, skillName, "SKILL.md");
|
|
16892
|
+
if (!existsSync(file)) throw new Error(`Пользовательский skill не найден: ${skillName}`);
|
|
16893
|
+
const current = await readFile(file, "utf8");
|
|
16894
|
+
const meta = readSkillMeta(file);
|
|
16895
|
+
const description = String(args.description || meta.description || `Пользовательский skill: ${skillName}`).trim();
|
|
16896
|
+
const instructions = String(args.instructions || args.text || args.prompt || stripFrontmatter(current)).trim();
|
|
16897
|
+
const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions));
|
|
16898
|
+
const body = buildUserSkillMarkdown({ name: skillName, description, instructions, tools });
|
|
16899
|
+
await writeFile(file, body, "utf8");
|
|
16900
|
+
if (args.enable) await userSkillSetEnabled(skillName, true);
|
|
16901
|
+
return { name: skillName, description, file, tools, status: "updated" };
|
|
15970
16902
|
}
|
|
15971
16903
|
|
|
15972
16904
|
async function userSkillSetEnabled(name, enabled) {
|
|
@@ -16060,9 +16992,23 @@ function inferUserSkillTools(question) {
|
|
|
16060
16992
|
}
|
|
16061
16993
|
if (/(календар|событи|встреч|телемост)/iu.test(text)) {
|
|
16062
16994
|
tools.add("yandex_calendar_list");
|
|
16995
|
+
tools.add("yandex_calendar_search");
|
|
16063
16996
|
tools.add("yandex_calendar_create_event");
|
|
16997
|
+
tools.add("yandex_calendar_update");
|
|
16998
|
+
tools.add("yandex_calendar_move");
|
|
16999
|
+
tools.add("yandex_calendar_delete");
|
|
17000
|
+
tools.add("yandex_calendar_add_reminder");
|
|
16064
17001
|
tools.add("yandex_telemost_create_event");
|
|
16065
17002
|
}
|
|
17003
|
+
if (/(документ|docs|360)/iu.test(text)) {
|
|
17004
|
+
tools.add("yandex_docs_list");
|
|
17005
|
+
tools.add("yandex_docs_find");
|
|
17006
|
+
tools.add("yandex_docs_create_text");
|
|
17007
|
+
tools.add("yandex_docs_read");
|
|
17008
|
+
tools.add("yandex_docs_share");
|
|
17009
|
+
tools.add("yandex_docs_rename");
|
|
17010
|
+
tools.add("yandex_docs_delete");
|
|
17011
|
+
}
|
|
16066
17012
|
if (/(контакт|адресн)/iu.test(text)) {
|
|
16067
17013
|
tools.add("yandex_contacts_search");
|
|
16068
17014
|
tools.add("yandex_contacts_get");
|
|
@@ -16103,6 +17049,69 @@ function inferUserSkillTools(question) {
|
|
|
16103
17049
|
return [...tools];
|
|
16104
17050
|
}
|
|
16105
17051
|
|
|
17052
|
+
function userSkillTemplates() {
|
|
17053
|
+
return [
|
|
17054
|
+
{
|
|
17055
|
+
name: "mail-triage",
|
|
17056
|
+
description: "Разбор почты: найти важные письма, прочитать, сохранить, создать задачу или событие.",
|
|
17057
|
+
tools: ["yandex_mail_list", "yandex_mail_search", "yandex_mail_read", "yandex_mail_save_to_disk", "yandex_mail_create_calendar_event", "yandex_mail_create_task"],
|
|
17058
|
+
instructions: "Помогай разбирать почту пользователя. Сначала показывай краткую сводку, затем выполняй только явно запрошенные действия: чтение, сохранение письма на Диск, создание события или задачи.",
|
|
17059
|
+
},
|
|
17060
|
+
{
|
|
17061
|
+
name: "family-calendar",
|
|
17062
|
+
description: "Семейный календарь: события, напоминания, встречи и ежедневные проверки.",
|
|
17063
|
+
tools: ["yandex_calendar_list", "yandex_calendar_search", "yandex_calendar_create_event", "yandex_calendar_move", "yandex_calendar_add_reminder", "yandex_calendar_delete", "yandex_calendar_reminders_tick"],
|
|
17064
|
+
instructions: "Помогай вести личный календарь. Для изменения календаря требуй явную просьбу пользователя. Всегда называй дату и время события.",
|
|
17065
|
+
},
|
|
17066
|
+
{
|
|
17067
|
+
name: "docs-organizer",
|
|
17068
|
+
description: "Организация документов на Яндекс Диске.",
|
|
17069
|
+
tools: ["yandex_docs_list", "yandex_docs_find", "yandex_docs_create_text", "yandex_docs_read", "yandex_docs_share", "yandex_docs_rename", "yandex_docs_delete", "yandex_disk_maintenance_tick"],
|
|
17070
|
+
instructions: "Помогай искать, создавать и приводить в порядок документы на Яндекс Диске. Не путай облачные документы с локальными файлами на компьютере.",
|
|
17071
|
+
},
|
|
17072
|
+
{
|
|
17073
|
+
name: "contact-workflow",
|
|
17074
|
+
description: "Работа с контактами, письмами, встречами и папками контактов.",
|
|
17075
|
+
tools: ["yandex_contacts_search", "yandex_contacts_get", "yandex_contact_send_mail", "yandex_contact_send_disk_link_qr", "yandex_contact_create_disk_folder", "yandex_contact_create_calendar_event", "yandex_contact_full_pack"],
|
|
17076
|
+
instructions: "Помогай выполнять действия вокруг контакта: найти карточку, отправить письмо, создать папку, отправить ссылку или создать встречу. Если найдено несколько контактов, проси уточнение.",
|
|
17077
|
+
},
|
|
17078
|
+
];
|
|
17079
|
+
}
|
|
17080
|
+
|
|
17081
|
+
function getUserSkillTemplate(name) {
|
|
17082
|
+
const normalized = normalizeUserSkillName(name || "");
|
|
17083
|
+
return userSkillTemplates().find((item) => item.name === normalized) || null;
|
|
17084
|
+
}
|
|
17085
|
+
|
|
17086
|
+
function buildUserSkillPreview(args = {}) {
|
|
17087
|
+
const name = normalizeUserSkillName(args.name || args.skill || args.title || "user-skill");
|
|
17088
|
+
const template = getUserSkillTemplate(args.template);
|
|
17089
|
+
const description = args.description || template?.description || `Пользовательский skill: ${name}`;
|
|
17090
|
+
const instructions = args.instructions || args.text || args.prompt || template?.instructions || "Опишите, что должен делать skill.";
|
|
17091
|
+
const tools = [...new Set([...(template?.tools || []), ...parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions))])];
|
|
17092
|
+
return buildUserSkillMarkdown({ name, description, instructions, tools });
|
|
17093
|
+
}
|
|
17094
|
+
|
|
17095
|
+
async function userSkillValidate(name) {
|
|
17096
|
+
const config = await loadConfig();
|
|
17097
|
+
const skill = findSkill(normalizeUserSkillName(name), config);
|
|
17098
|
+
const checks = [];
|
|
17099
|
+
if (!skill) {
|
|
17100
|
+
return { status: "error", checks: [{ check: "exists", status: "error", message: `Skill не найден: ${name || "-"}` }] };
|
|
17101
|
+
}
|
|
17102
|
+
const text = await readFile(skill.file, "utf8");
|
|
17103
|
+
const meta = readSkillMeta(skill.file);
|
|
17104
|
+
checks.push({ check: "exists", status: "ok", message: skill.file });
|
|
17105
|
+
checks.push({ check: "name", status: meta.name ? "ok" : "error", message: meta.name || "Нет name во frontmatter" });
|
|
17106
|
+
checks.push({ check: "description", status: meta.description ? "ok" : "warn", message: meta.description || "Описание пустое" });
|
|
17107
|
+
checks.push({ check: "instructions", status: stripFrontmatter(text).trim().length >= 40 ? "ok" : "warn", message: "Инструкции должны быть понятными и достаточно подробными" });
|
|
17108
|
+
checks.push({ check: "secrets", status: /(token|api[_-]?key|парол|секрет)\s*[:=]\s*\S{8,}/iu.test(text) ? "error" : "ok", message: "Skill не должен содержать секреты" });
|
|
17109
|
+
const tools = [...text.matchAll(/`([a-z0-9_:-]+)`/giu)].map((match) => match[1]).filter((tool) => ALL_TOOL_ALIASES.includes(tool) || tool.startsWith("mcp:"));
|
|
17110
|
+
const unknownTools = tools.filter((tool) => !ALL_TOOL_ALIASES.includes(tool) && !tool.startsWith("mcp:"));
|
|
17111
|
+
checks.push({ check: "tools", status: unknownTools.length ? "warn" : "ok", message: unknownTools.length ? `Неизвестные tools: ${unknownTools.join(", ")}` : `${tools.length} tools` });
|
|
17112
|
+
return { status: checks.some((row) => row.status === "error") ? "error" : "ok", checks };
|
|
17113
|
+
}
|
|
17114
|
+
|
|
16106
17115
|
function buildUserSkillMarkdown({ name, description, instructions, tools = [] }) {
|
|
16107
17116
|
const toolLines = tools.length
|
|
16108
17117
|
? ["", "Разрешенные/ожидаемые tools для этого skill:", "", ...tools.map((tool) => `- \`${tool}\``)]
|