@iola_adm/iola-cli 0.2.34 → 0.2.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/skills/yandex-services/SKILL.md +36 -4
- package/src/cli.js +1238 -40
- package/wiki/Daemon-RPC-/320/270-cron.md +47 -29
- package/wiki/Skills-/320/270-toolsets.md +20 -3
- package/wiki/Yandex-Connector.md +58 -3
- 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 +84 -2
package/src/cli.js
CHANGED
|
@@ -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,9 +192,27 @@ 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",
|
|
197
|
+
"yandex_calendar_calendars",
|
|
196
198
|
"yandex_calendar_create_event",
|
|
197
199
|
"yandex_calendar_list",
|
|
200
|
+
"yandex_calendar_get",
|
|
201
|
+
"yandex_calendar_search",
|
|
202
|
+
"yandex_calendar_update",
|
|
203
|
+
"yandex_calendar_move",
|
|
204
|
+
"yandex_calendar_delete",
|
|
205
|
+
"yandex_calendar_create_recurring_event",
|
|
206
|
+
"yandex_calendar_add_reminder",
|
|
207
|
+
"yandex_docs_status",
|
|
208
|
+
"yandex_docs_list",
|
|
209
|
+
"yandex_docs_find",
|
|
210
|
+
"yandex_docs_create_text",
|
|
211
|
+
"yandex_docs_read",
|
|
212
|
+
"yandex_docs_share",
|
|
213
|
+
"yandex_docs_rename",
|
|
214
|
+
"yandex_docs_delete",
|
|
215
|
+
"yandex_docs_save_answer",
|
|
198
216
|
"yandex_contacts_status",
|
|
199
217
|
"yandex_contacts_list",
|
|
200
218
|
"yandex_contacts_search",
|
|
@@ -224,7 +242,12 @@ const YANDEX_TOOLS = [
|
|
|
224
242
|
"yandex_contact_create_calendar_event",
|
|
225
243
|
"yandex_contact_create_telemost_event",
|
|
226
244
|
"yandex_contact_from_public_entity",
|
|
245
|
+
"yandex_telemost_status",
|
|
227
246
|
"yandex_telemost_create_event",
|
|
247
|
+
"yandex_contact_full_pack",
|
|
248
|
+
"yandex_daily_digest",
|
|
249
|
+
"yandex_calendar_reminders_tick",
|
|
250
|
+
"yandex_disk_maintenance_tick",
|
|
228
251
|
];
|
|
229
252
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
|
|
230
253
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
@@ -3047,6 +3070,29 @@ async function handleSkills(args) {
|
|
|
3047
3070
|
return;
|
|
3048
3071
|
}
|
|
3049
3072
|
|
|
3073
|
+
if (action === "templates") {
|
|
3074
|
+
printTable(userSkillTemplates().map((row) => ({ name: row.name, description: row.description, tools: row.tools.join(", ") })), [["name", "Шаблон"], ["description", "Описание"], ["tools", "Tools"]]);
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
if (action === "preview") {
|
|
3079
|
+
const options = parseOptions(args.slice(2));
|
|
3080
|
+
console.log(buildUserSkillPreview({
|
|
3081
|
+
name,
|
|
3082
|
+
description: options.description || "",
|
|
3083
|
+
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3084
|
+
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3085
|
+
template: options.template,
|
|
3086
|
+
}));
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
if (action === "validate") {
|
|
3091
|
+
const result = await userSkillValidate(name);
|
|
3092
|
+
printTable(result.checks, [["check", "Проверка"], ["status", "Статус"], ["message", "Сообщение"]]);
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3050
3096
|
if (action === "create" || action === "new") {
|
|
3051
3097
|
const options = parseOptions(args.slice(2));
|
|
3052
3098
|
const result = await userSkillCreate({
|
|
@@ -3054,6 +3100,7 @@ async function handleSkills(args) {
|
|
|
3054
3100
|
description: options.description || "",
|
|
3055
3101
|
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3056
3102
|
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3103
|
+
template: options.template,
|
|
3057
3104
|
enable: Boolean(options.enable),
|
|
3058
3105
|
overwrite: Boolean(options.force),
|
|
3059
3106
|
confirm: true,
|
|
@@ -3064,6 +3111,20 @@ async function handleSkills(args) {
|
|
|
3064
3111
|
return;
|
|
3065
3112
|
}
|
|
3066
3113
|
|
|
3114
|
+
if (action === "update" || action === "edit") {
|
|
3115
|
+
const options = parseOptions(args.slice(2));
|
|
3116
|
+
const result = await userSkillUpdate(name, {
|
|
3117
|
+
description: options.description,
|
|
3118
|
+
instructions: options.instructions || options.text || options.prompt || options._.join(" "),
|
|
3119
|
+
tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
|
|
3120
|
+
enable: options.enable,
|
|
3121
|
+
confirm: true,
|
|
3122
|
+
});
|
|
3123
|
+
console.log(`Skill обновлен: ${result.name}`);
|
|
3124
|
+
console.log(`Файл: ${result.file}`);
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3067
3128
|
if (action === "delete" || action === "remove" || action === "rm") {
|
|
3068
3129
|
const options = parseOptions(args.slice(2));
|
|
3069
3130
|
const result = await userSkillDelete(name, { confirm: Boolean(options.yes || options.force) });
|
|
@@ -3081,7 +3142,7 @@ async function handleSkills(args) {
|
|
|
3081
3142
|
return;
|
|
3082
3143
|
}
|
|
3083
3144
|
|
|
3084
|
-
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.");
|
|
3145
|
+
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.");
|
|
3085
3146
|
}
|
|
3086
3147
|
|
|
3087
3148
|
async function handleTools(args) {
|
|
@@ -3377,6 +3438,21 @@ async function handleYandex(args) {
|
|
|
3377
3438
|
return;
|
|
3378
3439
|
}
|
|
3379
3440
|
|
|
3441
|
+
if (action === "daily-digest" || action === "digest") {
|
|
3442
|
+
await handleYandexDailyDigest([target, ...rest].filter(Boolean));
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
if (action === "calendar-reminders" || action === "calendar-watch" || action === "reminders") {
|
|
3447
|
+
await handleYandexCalendarReminders([target, ...rest].filter(Boolean));
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
if (action === "disk-maintenance" || action === "disk-watch" || action === "disk-doctor") {
|
|
3452
|
+
await handleYandexDiskMaintenance([target, ...rest].filter(Boolean));
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3380
3456
|
if (action === "contacts-maintenance" || action === "contacts-watch" || action === "contacts-doctor") {
|
|
3381
3457
|
await handleYandexContactsMaintenance([target, ...rest].filter(Boolean));
|
|
3382
3458
|
return;
|
|
@@ -3418,6 +3494,9 @@ async function handleYandex(args) {
|
|
|
3418
3494
|
iola yandex status|doctor
|
|
3419
3495
|
iola yandex services
|
|
3420
3496
|
iola yandex mail-watch on|off|status|tick [--minutes 5]
|
|
3497
|
+
iola yandex daily-digest on|off|status|tick [--time 09:00] [--email]
|
|
3498
|
+
iola yandex calendar-reminders on|off|status|tick [--minutes 15]
|
|
3499
|
+
iola yandex disk-maintenance on|off|status|tick [--days 7]
|
|
3421
3500
|
iola yandex contacts-maintenance on|off|status|tick [--days 7] [--backup]
|
|
3422
3501
|
iola yandex enable disk mail calendar
|
|
3423
3502
|
iola yandex disable mail
|
|
@@ -3489,6 +3568,107 @@ async function handleYandexMailWatch(args = []) {
|
|
|
3489
3568
|
throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
|
|
3490
3569
|
}
|
|
3491
3570
|
|
|
3571
|
+
async function handleYandexDailyDigest(args = []) {
|
|
3572
|
+
const [action = "status", ...rest] = args;
|
|
3573
|
+
const options = parseOptions(rest);
|
|
3574
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3575
|
+
const time = options.time || rest.find((item) => /^\d{1,2}:\d{2}$/u.test(String(item))) || "09:00";
|
|
3576
|
+
const result = await yandexDailyDigestEnable({ time, email: Boolean(options.email), save: options.save !== false });
|
|
3577
|
+
console.log(`Ежедневный дайджест включен: каждый день ${result.time}. Email: ${result.email ? "yes" : "no"}.`);
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3581
|
+
await yandexDailyDigestDisable();
|
|
3582
|
+
console.log("Ежедневный дайджест выключен.");
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3586
|
+
const result = await yandexDailyDigestTick({ force: true, email: Boolean(options.email), save: options.save !== false });
|
|
3587
|
+
console.log(result.text || "Дайджест пуст.");
|
|
3588
|
+
if (result.remote) console.log(`Сохранено на Диск: ${result.remote}`);
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
const config = await loadConfig();
|
|
3592
|
+
const digest = config.yandex?.dailyDigest || {};
|
|
3593
|
+
printKeyValue({
|
|
3594
|
+
enabled: digest.enabled ? "yes" : "no",
|
|
3595
|
+
time: digest.time || "-",
|
|
3596
|
+
email: digest.email ? "yes" : "no",
|
|
3597
|
+
lastRunAt: digest.lastRunAt || "-",
|
|
3598
|
+
lastRemote: digest.lastRemote || "-",
|
|
3599
|
+
cron: listCronJobs().some((job) => job.command === "yandex daily-digest tick") ? "yes" : "no",
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
async function handleYandexCalendarReminders(args = []) {
|
|
3604
|
+
const [action = "status", ...rest] = args;
|
|
3605
|
+
const options = parseOptions(rest);
|
|
3606
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3607
|
+
const minutes = Math.max(1, Number(options.minutes || rest.find((item) => /^\d+$/u.test(String(item))) || 15));
|
|
3608
|
+
const result = await yandexCalendarRemindersEnable(minutes);
|
|
3609
|
+
console.log(`Проверка календарных напоминаний включена: каждые ${result.minutes} минут.`);
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3613
|
+
await yandexCalendarRemindersDisable();
|
|
3614
|
+
console.log("Проверка календарных напоминаний выключена.");
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3618
|
+
const result = await yandexCalendarRemindersTick({ force: true, horizonMinutes: Number(options.horizon || options.minutes || 60) });
|
|
3619
|
+
if (!result.events.length) console.log("Ближайших событий для напоминания нет.");
|
|
3620
|
+
else console.log(["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n"));
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
const config = await loadConfig();
|
|
3624
|
+
const reminders = config.yandex?.calendarReminders || {};
|
|
3625
|
+
printKeyValue({
|
|
3626
|
+
enabled: reminders.enabled ? "yes" : "no",
|
|
3627
|
+
minutes: reminders.minutes || "-",
|
|
3628
|
+
horizonMinutes: reminders.horizonMinutes || 60,
|
|
3629
|
+
lastRunAt: reminders.lastRunAt || "-",
|
|
3630
|
+
lastCount: reminders.lastCount ?? "-",
|
|
3631
|
+
cron: listCronJobs().some((job) => job.command === "yandex calendar-reminders tick") ? "yes" : "no",
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
async function handleYandexDiskMaintenance(args = []) {
|
|
3636
|
+
const [action = "status", ...rest] = args;
|
|
3637
|
+
const options = parseOptions(rest);
|
|
3638
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3639
|
+
const days = Math.max(1, Number(options.days || rest.find((item) => /^\d+$/u.test(String(item))) || 7));
|
|
3640
|
+
const result = await yandexDiskMaintenanceEnable(days);
|
|
3641
|
+
console.log(`Проверка Яндекс Диска включена: каждые ${result.days} дней.`);
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3645
|
+
await yandexDiskMaintenanceDisable();
|
|
3646
|
+
console.log("Проверка Яндекс Диска выключена.");
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3650
|
+
const result = await yandexDiskMaintenanceTick({ force: true });
|
|
3651
|
+
printKeyValue({
|
|
3652
|
+
used: formatBytes(result.usedSpace),
|
|
3653
|
+
total: formatBytes(result.totalSpace),
|
|
3654
|
+
trash: formatBytes(result.trashSize),
|
|
3655
|
+
docs: result.docs,
|
|
3656
|
+
publicLinks: result.publicLinks,
|
|
3657
|
+
remote: result.remote || "-",
|
|
3658
|
+
});
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
const config = await loadConfig();
|
|
3662
|
+
const disk = config.yandex?.diskMaintenance || {};
|
|
3663
|
+
printKeyValue({
|
|
3664
|
+
enabled: disk.enabled ? "yes" : "no",
|
|
3665
|
+
days: disk.days || "-",
|
|
3666
|
+
lastRunAt: disk.lastRunAt || "-",
|
|
3667
|
+
lastRemote: disk.lastRemote || "-",
|
|
3668
|
+
cron: listCronJobs().some((job) => job.command === "yandex disk-maintenance tick") ? "yes" : "no",
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3492
3672
|
async function handleYandexContactsMaintenance(args = []) {
|
|
3493
3673
|
const [action = "status", ...rest] = args;
|
|
3494
3674
|
const options = parseOptions(rest);
|
|
@@ -4146,9 +4326,27 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4146
4326
|
if (tool === "yandex_mail_city_context") return yandexMailCityContext(args.uid || args.id, args);
|
|
4147
4327
|
if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
|
|
4148
4328
|
if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
|
|
4329
|
+
if (tool === "yandex_mail_meeting_pack") return yandexMailMeetingPack(args.uid || args.id, args);
|
|
4149
4330
|
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
4331
|
+
if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
|
|
4150
4332
|
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
4151
4333
|
if (tool === "yandex_calendar_list") return yandexCalendarList(args);
|
|
4334
|
+
if (tool === "yandex_calendar_get") return yandexCalendarGet(args.query || args.uid || args.title || "", args);
|
|
4335
|
+
if (tool === "yandex_calendar_search") return yandexCalendarSearch(args.query || args.title || "", args);
|
|
4336
|
+
if (tool === "yandex_calendar_update") return yandexCalendarUpdate(args.query || args.uid || args.title || "", args);
|
|
4337
|
+
if (tool === "yandex_calendar_move") return yandexCalendarMove(args.query || args.uid || args.title || "", args);
|
|
4338
|
+
if (tool === "yandex_calendar_delete") return yandexCalendarDelete(args.query || args.uid || args.title || "", args);
|
|
4339
|
+
if (tool === "yandex_calendar_create_recurring_event") return yandexCalendarCreateRecurringEvent(args);
|
|
4340
|
+
if (tool === "yandex_calendar_add_reminder") return yandexCalendarAddReminder(args.query || args.uid || args.title || "", args);
|
|
4341
|
+
if (tool === "yandex_docs_status") return yandexDocsStatus();
|
|
4342
|
+
if (tool === "yandex_docs_list") return yandexDocsList(args);
|
|
4343
|
+
if (tool === "yandex_docs_find") return yandexDocsFind(args.query || args.name || "", args);
|
|
4344
|
+
if (tool === "yandex_docs_create_text") return yandexDocsCreateText(args);
|
|
4345
|
+
if (tool === "yandex_docs_read") return yandexDocsRead(args.path || args.remotePath || args.query || args.name, args);
|
|
4346
|
+
if (tool === "yandex_docs_share") return yandexDocsShare(args.path || args.remotePath || args.query || args.name, args);
|
|
4347
|
+
if (tool === "yandex_docs_rename") return yandexDocsRename(args.path || args.remotePath || args.query || args.name, args.name || args.newName || args.to, args);
|
|
4348
|
+
if (tool === "yandex_docs_delete") return yandexDocsDelete(args.path || args.remotePath || args.query || args.name, args);
|
|
4349
|
+
if (tool === "yandex_docs_save_answer") return yandexDocsCreateText({ ...args, text: args.text || args.answer || args.content });
|
|
4152
4350
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
4153
4351
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
4154
4352
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
@@ -4178,7 +4376,12 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4178
4376
|
if (tool === "yandex_contact_create_calendar_event") return yandexContactCreateCalendarEvent(args);
|
|
4179
4377
|
if (tool === "yandex_contact_create_telemost_event") return yandexContactCreateTelemostEvent(args);
|
|
4180
4378
|
if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
|
|
4379
|
+
if (tool === "yandex_telemost_status") return yandexTelemostStatus();
|
|
4181
4380
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
4381
|
+
if (tool === "yandex_contact_full_pack") return yandexContactFullPack(args);
|
|
4382
|
+
if (tool === "yandex_daily_digest") return yandexDailyDigestTick({ ...args, force: true });
|
|
4383
|
+
if (tool === "yandex_calendar_reminders_tick") return yandexCalendarRemindersTick({ ...args, force: true });
|
|
4384
|
+
if (tool === "yandex_disk_maintenance_tick") return yandexDiskMaintenanceTick({ ...args, force: true });
|
|
4182
4385
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
4183
4386
|
}
|
|
4184
4387
|
|
|
@@ -4349,6 +4552,142 @@ async function yandexMailWatchDisable() {
|
|
|
4349
4552
|
return { enabled: false };
|
|
4350
4553
|
}
|
|
4351
4554
|
|
|
4555
|
+
async function yandexDailyDigestEnable(options = {}) {
|
|
4556
|
+
const config = await loadConfig();
|
|
4557
|
+
const time = normalizeDigestTime(options.time || "09:00");
|
|
4558
|
+
await saveConfig({
|
|
4559
|
+
yandex: {
|
|
4560
|
+
...(config.yandex || {}),
|
|
4561
|
+
dailyDigest: { ...(config.yandex?.dailyDigest || {}), enabled: true, time, email: Boolean(options.email), save: options.save !== false, updatedAt: new Date().toISOString() },
|
|
4562
|
+
},
|
|
4563
|
+
});
|
|
4564
|
+
await upsertCronJob(`каждый день ${time}`, "yandex daily-digest tick", { replaceCommand: true });
|
|
4565
|
+
return { enabled: true, time, email: Boolean(options.email), save: options.save !== false };
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
async function yandexDailyDigestDisable() {
|
|
4569
|
+
const config = await loadConfig();
|
|
4570
|
+
const current = config.yandex?.dailyDigest || {};
|
|
4571
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
|
|
4572
|
+
deleteCronJobsByCommand("yandex daily-digest tick");
|
|
4573
|
+
return { enabled: false };
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
async function yandexDailyDigestTick(options = {}) {
|
|
4577
|
+
const config = await loadConfig();
|
|
4578
|
+
const digest = config.yandex?.dailyDigest || {};
|
|
4579
|
+
if (!digest.enabled && !options.force) return { enabled: false, text: "" };
|
|
4580
|
+
const unread = await yandexMailList({ mailbox: "INBOX", limit: 10, unread: true }).catch(() => []);
|
|
4581
|
+
const events = await yandexCalendarList({ start: new Date().toISOString(), end: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), limit: 20 }).catch(() => []);
|
|
4582
|
+
const incomplete = await yandexContactsFindIncomplete({ limit: 10 }).catch(() => []);
|
|
4583
|
+
const text = [
|
|
4584
|
+
`# Дайджест IOLA за ${new Intl.DateTimeFormat("ru-RU", { dateStyle: "long" }).format(new Date())}`,
|
|
4585
|
+
"",
|
|
4586
|
+
`Непрочитанных писем: ${unread.length}`,
|
|
4587
|
+
...unread.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`),
|
|
4588
|
+
"",
|
|
4589
|
+
`Событий на 24 часа: ${events.length}`,
|
|
4590
|
+
...events.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}`),
|
|
4591
|
+
"",
|
|
4592
|
+
`Неполных контактов: ${incomplete.length}`,
|
|
4593
|
+
...incomplete.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexContact(row)}`),
|
|
4594
|
+
].join("\n").trim();
|
|
4595
|
+
let remote = "";
|
|
4596
|
+
if (options.save !== false && digest.save !== false) {
|
|
4597
|
+
const saved = await yandexDocsCreateText({ title: `daily-digest-${timestampForFile()}`, text, format: "md", confirm: true });
|
|
4598
|
+
remote = saved.remote || "";
|
|
4599
|
+
}
|
|
4600
|
+
if (options.email || digest.email) {
|
|
4601
|
+
const profile = await getYandexIdentityProfile();
|
|
4602
|
+
const to = profile.defaultEmail || profile.emails?.[0];
|
|
4603
|
+
if (to) await yandexMailSend({ to: [to], subject: "Ежедневный дайджест IOLA", text, confirm: true });
|
|
4604
|
+
}
|
|
4605
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...digest, enabled: digest.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: remote || digest.lastRemote || "" } } });
|
|
4606
|
+
return { enabled: true, text, remote, unread: unread.length, events: events.length, incomplete: incomplete.length };
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
async function yandexCalendarRemindersEnable(minutes = 15) {
|
|
4610
|
+
const config = await loadConfig();
|
|
4611
|
+
const safeMinutes = Math.max(1, Number(minutes || 15));
|
|
4612
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...(config.yandex?.calendarReminders || {}), enabled: true, minutes: safeMinutes, horizonMinutes: 60, updatedAt: new Date().toISOString() } } });
|
|
4613
|
+
await upsertCronJob(`каждые ${safeMinutes} минут`, "yandex calendar-reminders tick", { replaceCommand: true });
|
|
4614
|
+
return { enabled: true, minutes: safeMinutes };
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
async function yandexCalendarRemindersDisable() {
|
|
4618
|
+
const config = await loadConfig();
|
|
4619
|
+
const current = config.yandex?.calendarReminders || {};
|
|
4620
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...current, enabled: false, seen: [], updatedAt: new Date().toISOString() } } });
|
|
4621
|
+
deleteCronJobsByCommand("yandex calendar-reminders tick");
|
|
4622
|
+
return { enabled: false };
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
async function yandexCalendarRemindersTick(options = {}) {
|
|
4626
|
+
const config = await loadConfig();
|
|
4627
|
+
const reminders = config.yandex?.calendarReminders || {};
|
|
4628
|
+
if (!reminders.enabled && !options.force) return { enabled: false, events: [] };
|
|
4629
|
+
const horizon = Math.max(5, Number(options.horizonMinutes || reminders.horizonMinutes || 60));
|
|
4630
|
+
const now = new Date();
|
|
4631
|
+
const rows = await yandexCalendarList({ start: now.toISOString(), end: new Date(now.getTime() + horizon * 60 * 1000).toISOString(), limit: 30 });
|
|
4632
|
+
const seen = new Set(reminders.seen || []);
|
|
4633
|
+
const fresh = rows.filter((row) => {
|
|
4634
|
+
const key = `${row.uid}:${row.start}`;
|
|
4635
|
+
if (seen.has(key)) return false;
|
|
4636
|
+
seen.add(key);
|
|
4637
|
+
return true;
|
|
4638
|
+
});
|
|
4639
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...reminders, enabled: reminders.enabled !== false, lastRunAt: new Date().toISOString(), lastCount: fresh.length, seen: [...seen].slice(-500) } } });
|
|
4640
|
+
return { enabled: true, events: fresh };
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
async function yandexDiskMaintenanceEnable(days = 7) {
|
|
4644
|
+
const config = await loadConfig();
|
|
4645
|
+
const safeDays = Math.max(1, Number(days || 7));
|
|
4646
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...(config.yandex?.diskMaintenance || {}), enabled: true, days: safeDays, updatedAt: new Date().toISOString() } } });
|
|
4647
|
+
await upsertCronJob(`каждые ${safeDays} дней`, "yandex disk-maintenance tick", { replaceCommand: true });
|
|
4648
|
+
return { enabled: true, days: safeDays };
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
async function yandexDiskMaintenanceDisable() {
|
|
4652
|
+
const config = await loadConfig();
|
|
4653
|
+
const current = config.yandex?.diskMaintenance || {};
|
|
4654
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
|
|
4655
|
+
deleteCronJobsByCommand("yandex disk-maintenance tick");
|
|
4656
|
+
return { enabled: false };
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
async function yandexDiskMaintenanceTick(options = {}) {
|
|
4660
|
+
const config = await loadConfig();
|
|
4661
|
+
const disk = config.yandex?.diskMaintenance || {};
|
|
4662
|
+
if (!disk.enabled && !options.force) return { enabled: false };
|
|
4663
|
+
const info = await yandexDiskInfo();
|
|
4664
|
+
const docs = await yandexDocsList({ limit: 500 }).catch(() => []);
|
|
4665
|
+
const all = await yandexDiskListRecursive(CLOUD_DEFAULT_REMOTE_DIR, { depth: 4, limit: 500 }).catch(() => []);
|
|
4666
|
+
const publicLinks = [];
|
|
4667
|
+
for (const row of all.slice(0, 100)) {
|
|
4668
|
+
const statRow = await yandexDiskStat(row.path).catch(() => null);
|
|
4669
|
+
if (statRow?.publicUrl) publicLinks.push(statRow);
|
|
4670
|
+
}
|
|
4671
|
+
const text = [
|
|
4672
|
+
`# Проверка Яндекс Диска ${new Date().toISOString()}`,
|
|
4673
|
+
"",
|
|
4674
|
+
`Занято: ${formatBytes(info.usedSpace)} из ${formatBytes(info.totalSpace)}`,
|
|
4675
|
+
`Корзина: ${formatBytes(info.trashSize)}`,
|
|
4676
|
+
`Документов: ${docs.length}`,
|
|
4677
|
+
`Публичных ссылок в /IOLA: ${publicLinks.length}`,
|
|
4678
|
+
...publicLinks.slice(0, 20).map((row, index) => `${index + 1}. ${row.path} — ${row.publicUrl}`),
|
|
4679
|
+
].join("\n");
|
|
4680
|
+
const saved = await yandexDocsCreateText({ title: `disk-maintenance-${timestampForFile()}`, text, format: "md", confirm: true });
|
|
4681
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...disk, enabled: disk.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: saved.remote || "", lastDocs: docs.length, lastPublicLinks: publicLinks.length } } });
|
|
4682
|
+
return { enabled: true, ...info, docs: docs.length, publicLinks: publicLinks.length, remote: saved.remote || "" };
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
function normalizeDigestTime(value) {
|
|
4686
|
+
const match = String(value || "").match(/^(\d{1,2})(?::(\d{2}))?$/u);
|
|
4687
|
+
if (!match) return "09:00";
|
|
4688
|
+
return `${String(Math.min(23, Number(match[1]))).padStart(2, "0")}:${String(Math.min(59, Number(match[2] || 0))).padStart(2, "0")}`;
|
|
4689
|
+
}
|
|
4690
|
+
|
|
4352
4691
|
async function yandexContactsMaintenanceEnable(days = 7, options = {}) {
|
|
4353
4692
|
const config = await loadConfig();
|
|
4354
4693
|
const safeDays = Math.max(1, Number(days || 7));
|
|
@@ -4552,6 +4891,47 @@ async function yandexMailCreateCalendarEvent(uid, args = {}) {
|
|
|
4552
4891
|
});
|
|
4553
4892
|
}
|
|
4554
4893
|
|
|
4894
|
+
async function yandexMailMeetingPack(uid, args = {}) {
|
|
4895
|
+
if (!args.confirm) throw new Error("Для пакетного сценария по письму нужен аргумент confirm=true.");
|
|
4896
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4897
|
+
const mailbox = await resolveYandexMailbox(args.mailbox || args.folder || "INBOX");
|
|
4898
|
+
const row = await yandexMailRead(uid, { mailbox, markSeen: true });
|
|
4899
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4900
|
+
const senderEmail = extractEmailAddress(row.from);
|
|
4901
|
+
const saved = await yandexMailSaveToDisk(uid, { mailbox });
|
|
4902
|
+
const shared = await yandexDiskShareWithQr(saved.remote, { confirm: true });
|
|
4903
|
+
const detected = extractDateTimeFromText(`${args.text || ""}\n${row.subject}\n${row.snippet}`);
|
|
4904
|
+
const start = args.start || detected.start || new Date(Date.now() + 3600000).toISOString();
|
|
4905
|
+
const end = args.end || detected.end || new Date(new Date(start).getTime() + 3600000).toISOString();
|
|
4906
|
+
const event = await yandexCalendarCreateEvent({
|
|
4907
|
+
title: args.title || `Встреча по письму: ${row.subject || `#${uid}`}`,
|
|
4908
|
+
description: [
|
|
4909
|
+
`Создано из письма #${uid}.`,
|
|
4910
|
+
`От: ${row.from || "-"}`,
|
|
4911
|
+
`Письмо сохранено: ${saved.remote}`,
|
|
4912
|
+
`Ссылка: ${shared.publicUrl}`,
|
|
4913
|
+
`QR-код: ${shared.qrPublicUrl}`,
|
|
4914
|
+
"",
|
|
4915
|
+
row.snippet || "",
|
|
4916
|
+
].join("\n"),
|
|
4917
|
+
location: args.location || detected.location || "",
|
|
4918
|
+
start,
|
|
4919
|
+
end,
|
|
4920
|
+
attendees: senderEmail ? [senderEmail] : [],
|
|
4921
|
+
confirm: true,
|
|
4922
|
+
});
|
|
4923
|
+
let sent = null;
|
|
4924
|
+
if ((args.send || args.email) && senderEmail) {
|
|
4925
|
+
sent = await yandexMailSend({
|
|
4926
|
+
to: [senderEmail],
|
|
4927
|
+
subject: args.subject || `Материалы к встрече: ${row.subject || `письмо #${uid}`}`,
|
|
4928
|
+
text: [`Создал встречу и сохранил письмо на Яндекс Диск.`, `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
|
|
4929
|
+
confirm: true,
|
|
4930
|
+
});
|
|
4931
|
+
}
|
|
4932
|
+
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 || [] };
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4555
4935
|
async function yandexMailSenderToContact(uid, args = {}) {
|
|
4556
4936
|
if (!args.confirm) throw new Error("Для добавления отправителя в контакты нужен аргумент confirm=true.");
|
|
4557
4937
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
@@ -4980,29 +5360,53 @@ async function yandexDavRequest(url, token, options = {}) {
|
|
|
4980
5360
|
|
|
4981
5361
|
async function yandexCalendarStatus() {
|
|
4982
5362
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4983
|
-
const
|
|
5363
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5364
|
+
const selected = pickYandexCalendar(calendars, {});
|
|
5365
|
+
const url = selected.url;
|
|
4984
5366
|
const text = await yandexDavRequest(url, token, { method: "PROPFIND", headers: { Depth: "0" }, xml: true, body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/></d:prop></d:propfind>" });
|
|
4985
|
-
return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || "calendar" };
|
|
5367
|
+
return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || selected.name || "calendar", calendars: calendars.length };
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
async function yandexCalendarCalendars(args = {}) {
|
|
5371
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5372
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5373
|
+
return calendars.slice(0, Number(args.limit || 50));
|
|
4986
5374
|
}
|
|
4987
5375
|
|
|
4988
5376
|
async function yandexCalendarCreateEvent(args = {}) {
|
|
4989
5377
|
if (!args.confirm) throw new Error("Для создания события в календаре нужен аргумент confirm=true.");
|
|
4990
5378
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4991
|
-
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
5379
|
+
const baseUrl = await yandexCalendarBaseUrl(token, args);
|
|
4992
5380
|
const uid = `${randomUUID()}@iola-cli`;
|
|
4993
5381
|
const start = toIcsDate(args.start || args.date || new Date(Date.now() + 3600000).toISOString());
|
|
4994
5382
|
const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
|
|
4995
5383
|
const summary = args.title || args.summary || "Событие IOLA";
|
|
4996
5384
|
const description = args.description || "";
|
|
4997
|
-
const ics = buildIcsEvent({
|
|
5385
|
+
const ics = buildIcsEvent({
|
|
5386
|
+
uid,
|
|
5387
|
+
start,
|
|
5388
|
+
end,
|
|
5389
|
+
summary,
|
|
5390
|
+
description,
|
|
5391
|
+
location: args.location || "",
|
|
5392
|
+
attendees: args.attendees || args.to || [],
|
|
5393
|
+
rrule: args.rrule || buildIcsRrule(args),
|
|
5394
|
+
reminders: normalizeCalendarReminders(args.reminders || args.reminder || args.alarm),
|
|
5395
|
+
});
|
|
4998
5396
|
const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
|
|
4999
5397
|
await yandexDavRequest(url, token, { method: "PUT", ics: true, body: ics, timeout: 45000 });
|
|
5000
|
-
return { status: "created", uid, title: summary, start: args.start || args.date || "", url };
|
|
5398
|
+
return { status: "calendar-event-created", uid, title: summary, start: args.start || args.date || "", end: args.end || "", url };
|
|
5399
|
+
}
|
|
5400
|
+
|
|
5401
|
+
async function yandexCalendarCreateRecurringEvent(args = {}) {
|
|
5402
|
+
if (!args.confirm) throw new Error("Для создания повторяющегося события нужен аргумент confirm=true.");
|
|
5403
|
+
const rrule = args.rrule || buildIcsRrule({ ...args, repeat: args.repeat || args.frequency || "weekly" });
|
|
5404
|
+
return yandexCalendarCreateEvent({ ...args, rrule, confirm: true });
|
|
5001
5405
|
}
|
|
5002
5406
|
|
|
5003
5407
|
async function yandexCalendarList(args = {}) {
|
|
5004
5408
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5005
|
-
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
5409
|
+
const baseUrl = await yandexCalendarBaseUrl(token, args);
|
|
5006
5410
|
const start = toIcsDate(args.start || new Date().toISOString());
|
|
5007
5411
|
const end = toIcsDate(args.end || new Date(Date.now() + 14 * 86400000).toISOString());
|
|
5008
5412
|
const body = `<?xml version="1.0"?>
|
|
@@ -5011,10 +5415,113 @@ async function yandexCalendarList(args = {}) {
|
|
|
5011
5415
|
<c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VEVENT"><c:time-range start="${start}" end="${end}"/></c:comp-filter></c:comp-filter></c:filter>
|
|
5012
5416
|
</c:calendar-query>`;
|
|
5013
5417
|
const text = await yandexDavRequest(baseUrl, token, { method: "REPORT", headers: { Depth: "1" }, xml: true, body, timeout: 45000 });
|
|
5014
|
-
return parseIcsEvents(text).slice(0, Number(args.limit || 20));
|
|
5418
|
+
return parseIcsEvents(text, { baseUrl }).slice(0, Number(args.limit || 20));
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5421
|
+
async function yandexCalendarSearch(query, args = {}) {
|
|
5422
|
+
const needle = normalizeGeoText(query || args.query || args.title || "");
|
|
5423
|
+
const rows = await yandexCalendarList({
|
|
5424
|
+
...args,
|
|
5425
|
+
start: args.start || new Date(Date.now() - 90 * 86400000).toISOString(),
|
|
5426
|
+
end: args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
5427
|
+
limit: Math.max(200, Number(args.limit || 20) * 10),
|
|
5428
|
+
});
|
|
5429
|
+
if (!needle) return rows.slice(0, Number(args.limit || 20));
|
|
5430
|
+
return rows.filter((row) => normalizeGeoText([
|
|
5431
|
+
row.title,
|
|
5432
|
+
row.description,
|
|
5433
|
+
row.location,
|
|
5434
|
+
row.uid,
|
|
5435
|
+
].filter(Boolean).join(" ")).includes(needle)).slice(0, Number(args.limit || 20));
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
async function yandexCalendarGet(query, args = {}) {
|
|
5439
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5440
|
+
if (resolved.status !== "ok") return resolved;
|
|
5441
|
+
return stripCalendarPrivateFields(resolved.event);
|
|
5015
5442
|
}
|
|
5016
5443
|
|
|
5017
|
-
async function
|
|
5444
|
+
async function yandexCalendarUpdate(query, args = {}) {
|
|
5445
|
+
if (!args.confirm) throw new Error("Для изменения события нужен аргумент confirm=true.");
|
|
5446
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5447
|
+
if (resolved.status !== "ok") return resolved;
|
|
5448
|
+
const event = resolved.event;
|
|
5449
|
+
const start = toIcsDate(args.start || args.date || event.startIso || event.start);
|
|
5450
|
+
const end = toIcsDate(args.end || event.endIso || event.end || new Date(new Date(args.start || event.startIso || Date.now()).getTime() + 3600000).toISOString());
|
|
5451
|
+
const next = buildIcsEvent({
|
|
5452
|
+
uid: event.uid || `${randomUUID()}@iola-cli`,
|
|
5453
|
+
start,
|
|
5454
|
+
end,
|
|
5455
|
+
summary: args.title || args.summary || event.title || "Событие IOLA",
|
|
5456
|
+
description: args.description ?? event.description ?? "",
|
|
5457
|
+
location: args.location ?? event.location ?? "",
|
|
5458
|
+
attendees: args.attendees || args.to || event.attendees || [],
|
|
5459
|
+
rrule: args.rrule === null ? "" : (args.rrule || event.rrule || buildIcsRrule(args)),
|
|
5460
|
+
reminders: args.reminders || args.reminder || args.alarm ? normalizeCalendarReminders(args.reminders || args.reminder || args.alarm) : event.reminders || [],
|
|
5461
|
+
});
|
|
5462
|
+
await yandexDavRequest(event.url, await requireYandexOAuthToken("organizer", "Яндекс Календарь"), { method: "PUT", ics: true, body: next, timeout: 45000 });
|
|
5463
|
+
return { status: "calendar-event-updated", uid: event.uid, title: args.title || event.title, href: event.href };
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5466
|
+
async function yandexCalendarMove(query, args = {}) {
|
|
5467
|
+
if (!args.confirm) throw new Error("Для переноса события нужен аргумент confirm=true.");
|
|
5468
|
+
const dateTime = args.start || args.date ? {} : extractDateTimeFromText(args.text || args.source_question || "");
|
|
5469
|
+
const start = args.start || args.date || dateTime.start;
|
|
5470
|
+
if (!start) throw new Error("Укажите новую дату/время события.");
|
|
5471
|
+
const end = args.end || dateTime.end || new Date(new Date(start).getTime() + 3600000).toISOString();
|
|
5472
|
+
return yandexCalendarUpdate(query, { ...args, start, end, confirm: true });
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
async function yandexCalendarAddReminder(query, args = {}) {
|
|
5476
|
+
if (!args.confirm) throw new Error("Для добавления напоминания нужен аргумент confirm=true.");
|
|
5477
|
+
const minutes = Number(args.minutes || args.beforeMinutes || String(args.reminder || "").match(/\d+/u)?.[0] || 15);
|
|
5478
|
+
return yandexCalendarUpdate(query, { ...args, reminders: [`-${minutes}`], confirm: true });
|
|
5479
|
+
}
|
|
5480
|
+
|
|
5481
|
+
async function yandexCalendarDelete(query, args = {}) {
|
|
5482
|
+
if (!args.confirm) throw new Error("Для удаления события нужен аргумент confirm=true.");
|
|
5483
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5484
|
+
if (resolved.status !== "ok") return resolved;
|
|
5485
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5486
|
+
await yandexDavRequest(resolved.event.url, token, { method: "DELETE", timeout: 45000 });
|
|
5487
|
+
return { status: "calendar-event-deleted", uid: resolved.event.uid, title: resolved.event.title, href: resolved.event.href };
|
|
5488
|
+
}
|
|
5489
|
+
|
|
5490
|
+
async function resolveYandexCalendarEvent(query, args = {}) {
|
|
5491
|
+
const uid = String(args.uid || "").trim();
|
|
5492
|
+
const href = String(args.href || "").trim();
|
|
5493
|
+
if (href) {
|
|
5494
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5495
|
+
const url = new URL(href, "https://caldav.yandex.ru/").toString();
|
|
5496
|
+
const ics = await yandexDavRequest(url, token, { method: "GET", timeout: 45000 });
|
|
5497
|
+
const event = parseIcsEvents(ics, { baseUrl: url.replace(/[^/]+$/u, "") })[0];
|
|
5498
|
+
return event ? { status: "ok", event: { ...event, href, url, ics } } : { status: "not-found", query: href };
|
|
5499
|
+
}
|
|
5500
|
+
const needle = normalizeGeoText(uid || query || args.query || args.title || "");
|
|
5501
|
+
const rows = await yandexCalendarSearch("", {
|
|
5502
|
+
...args,
|
|
5503
|
+
start: args.startRange || args.start || new Date(Date.now() - 365 * 86400000).toISOString(),
|
|
5504
|
+
end: args.endRange || args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
5505
|
+
limit: 500,
|
|
5506
|
+
});
|
|
5507
|
+
const matches = rows.filter((row) => {
|
|
5508
|
+
if (uid && row.uid === uid) return true;
|
|
5509
|
+
const haystack = normalizeGeoText([row.title, row.description, row.location, row.start, row.startIso, row.end, row.endIso, row.uid, row.href].filter(Boolean).join(" "));
|
|
5510
|
+
return needle && haystack.includes(needle);
|
|
5511
|
+
});
|
|
5512
|
+
if (!matches.length) return { status: "not-found", kind: "calendar-event", query: query || uid };
|
|
5513
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", kind: "calendar-event", query: query || uid, events: matches.slice(0, 10).map(stripCalendarPrivateFields) };
|
|
5514
|
+
return { status: "ok", event: matches[0] };
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
async function yandexCalendarBaseUrl(token, args = {}) {
|
|
5518
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5519
|
+
const selected = pickYandexCalendar(calendars, args);
|
|
5520
|
+
if (!selected) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
|
|
5521
|
+
return selected.url;
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
async function yandexCalendarCollections(token) {
|
|
5018
5525
|
const root = "https://caldav.yandex.ru/";
|
|
5019
5526
|
const principalXml = await yandexDavRequest(root, token, {
|
|
5020
5527
|
method: "PROPFIND",
|
|
@@ -5038,9 +5545,99 @@ async function yandexCalendarBaseUrl(token) {
|
|
|
5038
5545
|
xml: true,
|
|
5039
5546
|
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>",
|
|
5040
5547
|
});
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5548
|
+
return extractCalendarCollections(listXml).map((row) => ({ ...row, url: new URL(row.href, root).toString() }));
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5551
|
+
function pickYandexCalendar(calendars, args = {}) {
|
|
5552
|
+
const query = normalizeGeoText(args.calendar || args.calendarName || args.name || "");
|
|
5553
|
+
if (query) {
|
|
5554
|
+
const found = calendars.find((row) => normalizeGeoText(`${row.name} ${row.href}`).includes(query));
|
|
5555
|
+
if (found) return found;
|
|
5556
|
+
}
|
|
5557
|
+
return calendars.find((row) => !/\/(?:inbox|outbox)\//iu.test(row.href)) || calendars[0] || null;
|
|
5558
|
+
}
|
|
5559
|
+
|
|
5560
|
+
async function yandexDocsStatus() {
|
|
5561
|
+
const info = await yandexDiskInfo();
|
|
5562
|
+
await ensureYandexDiskDir(`${CLOUD_DEFAULT_REMOTE_DIR}/docs`, { allowExisting: true });
|
|
5563
|
+
return { status: "ok", provider: "yandex-disk", folder: `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, totalSpace: info.totalSpace, usedSpace: info.usedSpace };
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
async function yandexDocsList(args = {}) {
|
|
5567
|
+
const folder = normalizeCloudUserPath(args.path || args.folder || `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, "yandex-disk");
|
|
5568
|
+
const rows = await yandexDiskListRecursive(folder, { depth: Number(args.depth || 3), limit: Number(args.limit || 100) }).catch(() => []);
|
|
5569
|
+
return rows.filter(isYandexDocumentResource).slice(0, Number(args.limit || 50));
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5572
|
+
async function yandexDocsFind(query, args = {}) {
|
|
5573
|
+
const needle = normalizeGeoText(query || args.query || "");
|
|
5574
|
+
const rows = await yandexDocsList({ ...args, limit: Math.max(200, Number(args.limit || 20) * 10) });
|
|
5575
|
+
if (!needle) return rows.slice(0, Number(args.limit || 20));
|
|
5576
|
+
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.path}`).includes(needle)).slice(0, Number(args.limit || 20));
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
async function yandexDocsCreateText(args = {}) {
|
|
5580
|
+
if (!args.confirm) throw new Error("Для создания документа нужен аргумент confirm=true.");
|
|
5581
|
+
const text = String(args.text || args.content || "").trim();
|
|
5582
|
+
if (!text) throw new Error("Текст документа пустой.");
|
|
5583
|
+
const ext = normalizeYandexDocExtension(args.format || args.ext || path.extname(args.path || args.remotePath || ""));
|
|
5584
|
+
const title = slugYandexDiskName(args.title || args.name || `document-${timestampForFile()}`);
|
|
5585
|
+
const remotePath = normalizeCloudUserPath(args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/docs/${title}${title.endsWith(ext) ? "" : ext}`, "yandex-disk");
|
|
5586
|
+
const saved = await yandexDiskSaveText(text, remotePath);
|
|
5587
|
+
return { ...saved, status: "document-created", title: path.posix.basename(remotePath), remote: remotePath };
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5590
|
+
async function yandexDocsRead(target, args = {}) {
|
|
5591
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5592
|
+
if (doc.status !== "ok") return doc;
|
|
5593
|
+
if (!/\.(txt|md|csv|json|html)$/iu.test(doc.path)) {
|
|
5594
|
+
return { status: "binary-document", remote: doc.path, name: doc.name, message: "Этот документ не текстовый. Его можно скачать, переименовать, удалить или опубликовать ссылкой." };
|
|
5595
|
+
}
|
|
5596
|
+
return yandexDiskReadText(doc.path, args);
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
async function yandexDocsShare(target, args = {}) {
|
|
5600
|
+
if (!args.confirm) throw new Error("Для публикации документа нужен аргумент confirm=true.");
|
|
5601
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5602
|
+
if (doc.status !== "ok") return doc;
|
|
5603
|
+
return yandexDiskShareWithQr(doc.path, { ...args, confirm: true });
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5606
|
+
async function yandexDocsRename(target, newName, args = {}) {
|
|
5607
|
+
if (!args.confirm) throw new Error("Для переименования документа нужен аргумент confirm=true.");
|
|
5608
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5609
|
+
if (doc.status !== "ok") return doc;
|
|
5610
|
+
if (!newName) throw new Error("Укажите новое имя документа.");
|
|
5611
|
+
return yandexDiskRename(doc.path, newName, { ...args, confirm: true, overwrite: args.overwrite !== false });
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5614
|
+
async function yandexDocsDelete(target, args = {}) {
|
|
5615
|
+
if (!args.confirm) throw new Error("Для удаления документа нужен аргумент confirm=true.");
|
|
5616
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5617
|
+
if (doc.status !== "ok") return doc;
|
|
5618
|
+
return yandexDiskDelete(doc.path, args);
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
async function resolveYandexDoc(target, args = {}) {
|
|
5622
|
+
const value = String(target || "").trim();
|
|
5623
|
+
if (value.startsWith("/")) {
|
|
5624
|
+
const statRow = await yandexDiskStat(value);
|
|
5625
|
+
return isYandexDocumentResource(statRow) ? { status: "ok", ...statRow } : { status: "not-document", path: value };
|
|
5626
|
+
}
|
|
5627
|
+
const rows = await yandexDocsFind(value, { ...args, limit: 10 });
|
|
5628
|
+
if (!rows.length) return { status: "not-found", query: value };
|
|
5629
|
+
if (rows.length > 1 && !args.selectFirst) return { status: "ambiguous", query: value, docs: rows.slice(0, 10) };
|
|
5630
|
+
return { status: "ok", ...rows[0] };
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
function isYandexDocumentResource(row = {}) {
|
|
5634
|
+
return row.type !== "dir" && /\.(docx|xlsx|pptx|pdf|txt|md|html|csv|json)$/iu.test(row.name || row.path || "");
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
function normalizeYandexDocExtension(value) {
|
|
5638
|
+
const ext = String(value || "").replace(/^\./u, "").toLocaleLowerCase("en-US");
|
|
5639
|
+
if (["txt", "md", "html", "csv", "json"].includes(ext)) return `.${ext}`;
|
|
5640
|
+
return ".md";
|
|
5044
5641
|
}
|
|
5045
5642
|
|
|
5046
5643
|
async function yandexContactsStatus() {
|
|
@@ -5392,6 +5989,47 @@ async function yandexContactFromPublicEntity(args = {}) {
|
|
|
5392
5989
|
});
|
|
5393
5990
|
}
|
|
5394
5991
|
|
|
5992
|
+
async function yandexContactFullPack(args = {}) {
|
|
5993
|
+
if (!args.confirm) throw new Error("Для полного сценария по контакту нужен аргумент confirm=true.");
|
|
5994
|
+
const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
|
|
5995
|
+
if (resolved.status !== "ok") return resolved;
|
|
5996
|
+
const contact = resolved.contact;
|
|
5997
|
+
const folder = await yandexContactCreateDiskFolder({ query: contact.email || contact.name || contact.phone, confirm: true, selectFirst: true });
|
|
5998
|
+
const noteText = [
|
|
5999
|
+
`# ${contact.name || contact.email || contact.phone}`,
|
|
6000
|
+
"",
|
|
6001
|
+
`Email: ${contact.email || "-"}`,
|
|
6002
|
+
`Телефон: ${contact.phone || "-"}`,
|
|
6003
|
+
`Адрес: ${contact.address || "-"}`,
|
|
6004
|
+
`Организация: ${contact.org || "-"}`,
|
|
6005
|
+
"",
|
|
6006
|
+
args.note || args.text || "Пакет контакта создан IOLA CLI.",
|
|
6007
|
+
].join("\n");
|
|
6008
|
+
const doc = await yandexDocsCreateText({ path: path.posix.join(folder.remote, "meeting-note.md"), text: noteText, confirm: true });
|
|
6009
|
+
const shared = await yandexDiskShareWithQr(folder.remote, { confirm: true });
|
|
6010
|
+
const dateTime = args.start || args.date ? { start: args.start || args.date, end: args.end } : extractDateTimeFromText(args.text || args.source_question || "");
|
|
6011
|
+
let event = null;
|
|
6012
|
+
if (args.createEvent !== false && contact.email) {
|
|
6013
|
+
event = await yandexCalendarCreateEvent({
|
|
6014
|
+
...dateTime,
|
|
6015
|
+
title: args.title || `Встреча: ${contact.name || contact.email}`,
|
|
6016
|
+
description: [`Папка контакта: ${folder.remote}`, `Ссылка: ${shared.publicUrl}`, args.note || ""].filter(Boolean).join("\n"),
|
|
6017
|
+
attendees: [contact.email],
|
|
6018
|
+
confirm: true,
|
|
6019
|
+
});
|
|
6020
|
+
}
|
|
6021
|
+
let sent = null;
|
|
6022
|
+
if ((args.send || args.email) && contact.email) {
|
|
6023
|
+
sent = await yandexMailSend({
|
|
6024
|
+
to: [contact.email],
|
|
6025
|
+
subject: args.subject || `Материалы IOLA: ${contact.name || contact.email}`,
|
|
6026
|
+
text: [args.message || "Подготовил материалы.", `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
|
|
6027
|
+
confirm: true,
|
|
6028
|
+
});
|
|
6029
|
+
}
|
|
6030
|
+
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 || [] };
|
|
6031
|
+
}
|
|
6032
|
+
|
|
5395
6033
|
async function resolveYandexContact(query, args = {}) {
|
|
5396
6034
|
const rows = await yandexContactsList({ limit: Math.max(500, Number(args.limit || 100)), full: true });
|
|
5397
6035
|
const normalized = normalizeContactLookupText(query || args.query || args.name || args.email || args.phone || "");
|
|
@@ -5639,25 +6277,92 @@ async function yandexContactsBaseUrl(token) {
|
|
|
5639
6277
|
return new URL(addressBookHref, root).toString();
|
|
5640
6278
|
}
|
|
5641
6279
|
|
|
6280
|
+
async function yandexTelemostStatus() {
|
|
6281
|
+
const calendar = await yandexCalendarStatus();
|
|
6282
|
+
return {
|
|
6283
|
+
status: "calendar-fallback",
|
|
6284
|
+
provider: "yandex-telemost",
|
|
6285
|
+
calendar: calendar.displayName,
|
|
6286
|
+
message: "Для обычного OAuth-подключения CLI использует календарное событие. Прямой Telemost API проверяется отдельно и может быть доступен только аккаунтам/организациям Яндекс 360.",
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
|
|
5642
6290
|
async function yandexTelemostCreateEvent(args = {}) {
|
|
6291
|
+
if (!args.confirm) throw new Error("Для создания встречи нужен аргумент confirm=true.");
|
|
6292
|
+
const telemost = await tryCreateYandexTelemostMeeting(args).catch((error) => ({
|
|
6293
|
+
status: "telemost-api-unavailable",
|
|
6294
|
+
error: error instanceof Error ? error.message : String(error),
|
|
6295
|
+
}));
|
|
5643
6296
|
const description = [
|
|
5644
6297
|
args.description || "",
|
|
5645
6298
|
"",
|
|
5646
|
-
"Телемост:
|
|
6299
|
+
telemost.joinUrl ? `Ссылка Телемоста: ${telemost.joinUrl}` : "Телемост: прямое создание ссылки через API недоступно для текущего OAuth-подключения. Событие создано в календаре как встреча; ссылку можно добавить вручную в Яндекс Календаре.",
|
|
5647
6300
|
].join("\n").trim();
|
|
5648
|
-
|
|
6301
|
+
const event = await yandexCalendarCreateEvent({
|
|
6302
|
+
...args,
|
|
6303
|
+
title: args.title || args.summary || "Телемост IOLA",
|
|
6304
|
+
description,
|
|
6305
|
+
location: telemost.joinUrl || args.location || "Яндекс Телемост",
|
|
6306
|
+
confirm: true,
|
|
6307
|
+
});
|
|
6308
|
+
return { ...event, status: telemost.joinUrl ? "telemost-event-created" : "telemost-calendar-fallback-created", telemost };
|
|
5649
6309
|
}
|
|
5650
6310
|
|
|
5651
|
-
function
|
|
6311
|
+
async function tryCreateYandexTelemostMeeting(args = {}) {
|
|
6312
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Телемост");
|
|
6313
|
+
const endpoints = [
|
|
6314
|
+
"https://cloud-api.yandex.net/v1/telemost/meetings",
|
|
6315
|
+
"https://api360.yandex.net/v1/telemost/meetings",
|
|
6316
|
+
];
|
|
6317
|
+
let lastError = "";
|
|
6318
|
+
for (const endpoint of endpoints) {
|
|
6319
|
+
const response = await fetch(endpoint, {
|
|
6320
|
+
method: "POST",
|
|
6321
|
+
headers: {
|
|
6322
|
+
Authorization: `OAuth ${token}`,
|
|
6323
|
+
"content-type": "application/json",
|
|
6324
|
+
},
|
|
6325
|
+
body: JSON.stringify({
|
|
6326
|
+
title: args.title || args.summary || "Телемост IOLA",
|
|
6327
|
+
description: args.description || "",
|
|
6328
|
+
}),
|
|
6329
|
+
signal: AbortSignal.timeout(15000),
|
|
6330
|
+
}).catch((error) => ({ ok: false, status: 0, statusText: error.message, text: async () => error.message }));
|
|
6331
|
+
const text = await response.text().catch(() => "");
|
|
6332
|
+
if (response.ok) {
|
|
6333
|
+
const payload = text ? JSON.parse(text) : {};
|
|
6334
|
+
return {
|
|
6335
|
+
status: "telemost-api-created",
|
|
6336
|
+
endpoint,
|
|
6337
|
+
id: payload.id || payload.meeting_id || "",
|
|
6338
|
+
joinUrl: payload.join_url || payload.url || payload.link || payload.meeting_url || "",
|
|
6339
|
+
raw: payload,
|
|
6340
|
+
};
|
|
6341
|
+
}
|
|
6342
|
+
lastError = `${endpoint}: ${response.status} ${response.statusText} ${sanitizeSecretFromText(text.slice(0, 500), token)}`;
|
|
6343
|
+
if (![404, 405].includes(Number(response.status))) break;
|
|
6344
|
+
}
|
|
6345
|
+
throw new Error(lastError || "Telemost API недоступен.");
|
|
6346
|
+
}
|
|
6347
|
+
|
|
6348
|
+
function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [], rrule = "", reminders = [] }) {
|
|
5652
6349
|
const escape = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
5653
6350
|
const attendeeLines = (Array.isArray(attendees) ? attendees : [attendees])
|
|
5654
6351
|
.map((email) => String(email || "").trim())
|
|
5655
6352
|
.filter((email) => /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email))
|
|
5656
6353
|
.map((email) => `ATTENDEE;CN=${escape(email)};ROLE=REQ-PARTICIPANT:mailto:${email}`);
|
|
6354
|
+
const reminderBlocks = normalizeCalendarReminders(reminders).map((minutes) => [
|
|
6355
|
+
"BEGIN:VALARM",
|
|
6356
|
+
`TRIGGER:-PT${Math.max(1, Math.abs(Number(minutes) || 15))}M`,
|
|
6357
|
+
"ACTION:DISPLAY",
|
|
6358
|
+
`DESCRIPTION:${escape(summary || "Напоминание")}`,
|
|
6359
|
+
"END:VALARM",
|
|
6360
|
+
].join("\r\n"));
|
|
5657
6361
|
return [
|
|
5658
6362
|
"BEGIN:VCALENDAR",
|
|
5659
6363
|
"VERSION:2.0",
|
|
5660
6364
|
"PRODID:-//IOLA CLI//Yandex Calendar//RU",
|
|
6365
|
+
"CALSCALE:GREGORIAN",
|
|
5661
6366
|
"BEGIN:VEVENT",
|
|
5662
6367
|
`UID:${uid}`,
|
|
5663
6368
|
`DTSTAMP:${toIcsDate(new Date().toISOString())}`,
|
|
@@ -5666,7 +6371,9 @@ function buildIcsEvent({ uid, start, end, summary, description, location, attend
|
|
|
5666
6371
|
`SUMMARY:${escape(summary)}`,
|
|
5667
6372
|
description ? `DESCRIPTION:${escape(description)}` : "",
|
|
5668
6373
|
location ? `LOCATION:${escape(location)}` : "",
|
|
6374
|
+
rrule ? `RRULE:${rrule}` : "",
|
|
5669
6375
|
...attendeeLines,
|
|
6376
|
+
...reminderBlocks,
|
|
5670
6377
|
"END:VEVENT",
|
|
5671
6378
|
"END:VCALENDAR",
|
|
5672
6379
|
"",
|
|
@@ -5679,15 +6386,105 @@ function toIcsDate(value) {
|
|
|
5679
6386
|
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/u, "Z");
|
|
5680
6387
|
}
|
|
5681
6388
|
|
|
5682
|
-
function parseIcsEvents(xmlOrIcs) {
|
|
5683
|
-
const
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
6389
|
+
function parseIcsEvents(xmlOrIcs, options = {}) {
|
|
6390
|
+
const text = String(xmlOrIcs || "");
|
|
6391
|
+
const responses = text.split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
6392
|
+
if (responses.length) {
|
|
6393
|
+
return responses.flatMap((response) => {
|
|
6394
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
6395
|
+
const calendarData = response.match(/<[^:>]*:?calendar-data[^>]*>([\s\S]*?)<\/[^:>]*:?calendar-data>/iu)?.[1] || response;
|
|
6396
|
+
return parseIcsEvents(calendarData, options).map((row) => ({
|
|
6397
|
+
...row,
|
|
6398
|
+
href: row.href || href,
|
|
6399
|
+
url: row.url || (href ? new URL(href, options.baseUrl || "https://caldav.yandex.ru/").toString() : ""),
|
|
6400
|
+
}));
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
const decoded = decodeXml(stripXmlTags(text));
|
|
6404
|
+
return decoded.split("BEGIN:VEVENT").slice(1).map((chunk) => {
|
|
6405
|
+
const lines = unfoldIcsLines(`BEGIN:VEVENT${chunk}`);
|
|
6406
|
+
const uid = icsValue(lines, "UID");
|
|
6407
|
+
const start = icsValue(lines, "DTSTART");
|
|
6408
|
+
const end = icsValue(lines, "DTEND");
|
|
6409
|
+
return {
|
|
6410
|
+
uid,
|
|
6411
|
+
title: unescapeIcsValue(icsValue(lines, "SUMMARY")),
|
|
6412
|
+
start,
|
|
6413
|
+
end,
|
|
6414
|
+
startIso: icsDateToIso(start),
|
|
6415
|
+
endIso: icsDateToIso(end),
|
|
6416
|
+
location: unescapeIcsValue(icsValue(lines, "LOCATION")),
|
|
6417
|
+
description: unescapeIcsValue(icsValue(lines, "DESCRIPTION")),
|
|
6418
|
+
rrule: icsValue(lines, "RRULE"),
|
|
6419
|
+
attendees: icsValues(lines, "ATTENDEE").map((value) => value.replace(/^mailto:/iu, "")),
|
|
6420
|
+
reminders: parseIcsReminderMinutes(lines),
|
|
6421
|
+
ics: decoded.includes("BEGIN:VCALENDAR") ? decoded : "",
|
|
6422
|
+
};
|
|
6423
|
+
}).filter((item) => item.uid || item.title);
|
|
6424
|
+
}
|
|
6425
|
+
|
|
6426
|
+
function unfoldIcsLines(value) {
|
|
6427
|
+
return String(value || "")
|
|
6428
|
+
.replace(/\r/g, "")
|
|
6429
|
+
.replace(/\n[ \t]/g, "")
|
|
6430
|
+
.split(/\n/u)
|
|
6431
|
+
.map((line) => line.trim())
|
|
6432
|
+
.filter(Boolean);
|
|
6433
|
+
}
|
|
6434
|
+
|
|
6435
|
+
function icsValues(lines, property) {
|
|
6436
|
+
const prop = String(property || "").toLocaleUpperCase("en-US");
|
|
6437
|
+
return lines
|
|
6438
|
+
.filter((line) => line.toLocaleUpperCase("en-US").startsWith(`${prop};`) || line.toLocaleUpperCase("en-US").startsWith(`${prop}:`))
|
|
6439
|
+
.map((line) => line.slice(line.indexOf(":") + 1).trim())
|
|
6440
|
+
.filter(Boolean);
|
|
6441
|
+
}
|
|
6442
|
+
|
|
6443
|
+
function icsValue(lines, property) {
|
|
6444
|
+
return icsValues(lines, property)[0] || "";
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
function unescapeIcsValue(value) {
|
|
6448
|
+
return String(value || "")
|
|
6449
|
+
.replace(/\\n/giu, "\n")
|
|
6450
|
+
.replace(/\\,/gu, ",")
|
|
6451
|
+
.replace(/\\;/gu, ";")
|
|
6452
|
+
.replace(/\\\\/gu, "\\")
|
|
6453
|
+
.trim();
|
|
6454
|
+
}
|
|
6455
|
+
|
|
6456
|
+
function icsDateToIso(value) {
|
|
6457
|
+
const text = String(value || "").trim();
|
|
6458
|
+
const match = text.match(/^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/u);
|
|
6459
|
+
if (!match) return "";
|
|
6460
|
+
const iso = `${match[1]}-${match[2]}-${match[3]}T${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}${text.endsWith("Z") ? "Z" : ""}`;
|
|
6461
|
+
const date = new Date(iso);
|
|
6462
|
+
return Number.isNaN(date.getTime()) ? "" : date.toISOString();
|
|
6463
|
+
}
|
|
6464
|
+
|
|
6465
|
+
function parseIcsReminderMinutes(lines) {
|
|
6466
|
+
return lines
|
|
6467
|
+
.filter((line) => /^TRIGGER:/iu.test(line))
|
|
6468
|
+
.map((line) => Number(line.match(/PT(\d+)M/iu)?.[1] || 0))
|
|
6469
|
+
.filter(Boolean);
|
|
6470
|
+
}
|
|
6471
|
+
|
|
6472
|
+
function normalizeCalendarReminders(value) {
|
|
6473
|
+
const rows = Array.isArray(value) ? value : String(value || "").split(/[;,]/u);
|
|
6474
|
+
return rows.map((item) => Number(String(item).match(/\d+/u)?.[0] || 0)).filter(Boolean);
|
|
6475
|
+
}
|
|
6476
|
+
|
|
6477
|
+
function buildIcsRrule(args = {}) {
|
|
6478
|
+
const repeat = String(args.repeat || args.frequency || "").toLocaleLowerCase("ru-RU");
|
|
6479
|
+
if (!repeat && !args.count && !args.until) return "";
|
|
6480
|
+
const freq = /день|daily|day/iu.test(repeat) ? "DAILY"
|
|
6481
|
+
: /месяц|monthly|month/iu.test(repeat) ? "MONTHLY"
|
|
6482
|
+
: /год|year|yearly|annual/iu.test(repeat) ? "YEARLY"
|
|
6483
|
+
: "WEEKLY";
|
|
6484
|
+
const parts = [`FREQ=${freq}`];
|
|
6485
|
+
if (args.count) parts.push(`COUNT=${Number(args.count)}`);
|
|
6486
|
+
if (args.until) parts.push(`UNTIL=${toIcsDate(args.until)}`);
|
|
6487
|
+
return parts.join(";");
|
|
5691
6488
|
}
|
|
5692
6489
|
|
|
5693
6490
|
function extractDavHref(xml, propertyName) {
|
|
@@ -5706,6 +6503,24 @@ function extractCalendarCollectionHref(xml) {
|
|
|
5706
6503
|
return "";
|
|
5707
6504
|
}
|
|
5708
6505
|
|
|
6506
|
+
function extractCalendarCollections(xml) {
|
|
6507
|
+
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
6508
|
+
const rows = [];
|
|
6509
|
+
for (const response of responses) {
|
|
6510
|
+
if (!/<[^:>]*:?calendar(?:\s|>|\/)/iu.test(response)) continue;
|
|
6511
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
6512
|
+
if (!href || /\/(?:inbox|outbox)\//iu.test(href)) continue;
|
|
6513
|
+
const name = stripXmlTags(response.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "").trim() || path.posix.basename(href.replace(/\/$/u, ""));
|
|
6514
|
+
rows.push({ provider: "yandex-calendar", href, name, type: "calendar" });
|
|
6515
|
+
}
|
|
6516
|
+
return rows;
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
function stripCalendarPrivateFields(row = {}) {
|
|
6520
|
+
const { ics: _ics, url: _url, ...rest } = row;
|
|
6521
|
+
return rest;
|
|
6522
|
+
}
|
|
6523
|
+
|
|
5709
6524
|
function extractAddressBookCollectionHref(xml) {
|
|
5710
6525
|
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
5711
6526
|
for (const response of responses) {
|
|
@@ -10654,6 +11469,13 @@ function isCronDue(job) {
|
|
|
10654
11469
|
return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyDays[1]) * 24 * 60 * 60 * 1000;
|
|
10655
11470
|
}
|
|
10656
11471
|
if (normalized.includes("каждый день") || normalized.includes("daily")) {
|
|
11472
|
+
const time = normalized.match(/(\d{1,2}):(\d{2})/u);
|
|
11473
|
+
if (time) {
|
|
11474
|
+
const dueAt = new Date(now);
|
|
11475
|
+
dueAt.setHours(Number(time[1]), Number(time[2]), 0, 0);
|
|
11476
|
+
const ranToday = lastRun && lastRun.toISOString().slice(0, 10) === now.toISOString().slice(0, 10);
|
|
11477
|
+
return now >= dueAt && !ranToday;
|
|
11478
|
+
}
|
|
10657
11479
|
return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
|
|
10658
11480
|
}
|
|
10659
11481
|
if (normalized.includes("каждую неделю") || normalized.includes("weekly")) {
|
|
@@ -11205,6 +12027,49 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
11205
12027
|
].join("\n");
|
|
11206
12028
|
}
|
|
11207
12029
|
|
|
12030
|
+
if (/(дайджест|сводк)/iu.test(normalized) && /(яндекс|почт|календар|контакт|диск)/iu.test(normalized)) {
|
|
12031
|
+
if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized) && /(кажд|ежеднев|авто|регуляр)/iu.test(normalized)) {
|
|
12032
|
+
const time = question.match(/(\d{1,2}:\d{2})/u)?.[1] || "09:00";
|
|
12033
|
+
const result = await yandexDailyDigestEnable({ time, email: /email|почт[уы]|письм/iu.test(normalized), save: true });
|
|
12034
|
+
return `Ежедневный дайджест включен: каждый день ${result.time}.`;
|
|
12035
|
+
}
|
|
12036
|
+
if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
12037
|
+
await yandexDailyDigestDisable();
|
|
12038
|
+
return "Ежедневный дайджест выключен.";
|
|
12039
|
+
}
|
|
12040
|
+
const result = await yandexDailyDigestTick({ force: true, save: true, email: /отправь|email|почт[уы]/iu.test(normalized) });
|
|
12041
|
+
return [result.text, result.remote ? `\nСохранено: ${result.remote}` : ""].join("").trim();
|
|
12042
|
+
}
|
|
12043
|
+
|
|
12044
|
+
if (/(календар|событи|встреч)/iu.test(normalized) && /(напомин|уведом|следи|монитор|авто|регуляр)/iu.test(normalized)) {
|
|
12045
|
+
if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
12046
|
+
const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
|
|
12047
|
+
await yandexCalendarRemindersEnable(minutes);
|
|
12048
|
+
return `Проверка календарных напоминаний включена: каждые ${minutes} минут.`;
|
|
12049
|
+
}
|
|
12050
|
+
if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
12051
|
+
await yandexCalendarRemindersDisable();
|
|
12052
|
+
return "Проверка календарных напоминаний выключена.";
|
|
12053
|
+
}
|
|
12054
|
+
const result = await yandexCalendarRemindersTick({ force: true });
|
|
12055
|
+
if (!result.events.length) return "Ближайших событий для напоминания нет.";
|
|
12056
|
+
return ["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n");
|
|
12057
|
+
}
|
|
12058
|
+
|
|
12059
|
+
if (/(диск|яндекс.?диск|документ)/iu.test(normalized) && /(провер|обслуж|maintenance|аудит|регуляр|авто)/iu.test(normalized)) {
|
|
12060
|
+
if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
12061
|
+
const days = Number(question.match(/(\d+)\s*(?:дн|день|дня|дней)/iu)?.[1] || 7);
|
|
12062
|
+
await yandexDiskMaintenanceEnable(days);
|
|
12063
|
+
return `Проверка Яндекс Диска включена: каждые ${days} дней.`;
|
|
12064
|
+
}
|
|
12065
|
+
if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
12066
|
+
await yandexDiskMaintenanceDisable();
|
|
12067
|
+
return "Проверка Яндекс Диска выключена.";
|
|
12068
|
+
}
|
|
12069
|
+
const result = await yandexDiskMaintenanceTick({ force: true });
|
|
12070
|
+
return `Проверка Яндекс Диска выполнена. Документов: ${result.docs}, публичных ссылок: ${result.publicLinks}. Отчет: ${result.remote}.`;
|
|
12071
|
+
}
|
|
12072
|
+
|
|
11208
12073
|
if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
|
|
11209
12074
|
return await buildYandexContactsDirectAnswer(question, normalized);
|
|
11210
12075
|
}
|
|
@@ -11252,6 +12117,12 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
11252
12117
|
const result = await yandexMailSaveToDisk(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
|
|
11253
12118
|
return `Письмо #${uid} сохранено на Яндекс Диск: ${result.remote || result.path}.`;
|
|
11254
12119
|
}
|
|
12120
|
+
if (/(пакет|комплект|встреч).{0,80}(письм|письма)|(?:письм|письма).{0,80}(пакет|комплект|встреч)/iu.test(normalized) && /(диск|ссылк|qr|календар|встреч)/iu.test(normalized)) {
|
|
12121
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
12122
|
+
if (!uid) return "Из какого письма создать пакет? Укажите номер из списка или UID.";
|
|
12123
|
+
const result = await yandexMailMeetingPack(uid, { mailbox: extractYandexMailboxName(question) || "INBOX", ...extractDateTimeFromText(question), confirm: true });
|
|
12124
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12125
|
+
}
|
|
11255
12126
|
if (/(создай|добавь).{0,40}(событи|встреч|календар)/iu.test(normalized)) {
|
|
11256
12127
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
11257
12128
|
if (!uid) return "Из какого письма создать событие? Укажите номер из списка или UID.";
|
|
@@ -11356,10 +12227,93 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
11356
12227
|
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
11357
12228
|
}
|
|
11358
12229
|
|
|
11359
|
-
if (/(
|
|
12230
|
+
if (/(документ|документы|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
|
|
12231
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
12232
|
+
const result = await yandexDocsStatus();
|
|
12233
|
+
return `Яндекс Документы через Диск подключены. Папка: ${result.folder}.`;
|
|
12234
|
+
}
|
|
12235
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
|
|
12236
|
+
const text = extractShareMessage(question) || cleanupCloudSaveText(question);
|
|
12237
|
+
if (!text) return "Укажите текст документа. Пример: создай документ на Яндекс Диске текст: ...";
|
|
12238
|
+
const result = await yandexDocsCreateText({
|
|
12239
|
+
title: extractYandexDocTitle(question),
|
|
12240
|
+
text,
|
|
12241
|
+
format: /html/iu.test(normalized) ? "html" : /txt|текстов/iu.test(normalized) ? "txt" : "md",
|
|
12242
|
+
confirm: true,
|
|
12243
|
+
});
|
|
12244
|
+
return `Документ создан на Яндекс Диске: ${result.remote}.`;
|
|
12245
|
+
}
|
|
12246
|
+
if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
|
|
12247
|
+
const result = await yandexDocsRead(extractCloudPath(question) || cleanupYandexQuery(question), {});
|
|
12248
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12249
|
+
}
|
|
12250
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
|
|
12251
|
+
const result = await yandexDocsShare(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
|
|
12252
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12253
|
+
}
|
|
12254
|
+
if (/(переимен|rename)/iu.test(normalized)) {
|
|
12255
|
+
const result = await yandexDocsRename(extractCloudPath(question) || cleanupYandexQuery(question), extractCloudNewName(question), { confirm: true });
|
|
12256
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12257
|
+
}
|
|
12258
|
+
if (/(удали|удалить)/iu.test(normalized)) {
|
|
12259
|
+
const result = await yandexDocsDelete(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
|
|
12260
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12261
|
+
}
|
|
12262
|
+
const rows = /(найди|поиск)/iu.test(normalized)
|
|
12263
|
+
? await yandexDocsFind(cleanupYandexQuery(question), { limit: 20 })
|
|
12264
|
+
: await yandexDocsList({ limit: 20 });
|
|
12265
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
12266
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
12267
|
+
}
|
|
12268
|
+
|
|
12269
|
+
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
12270
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized) && /(календар|телемост)/iu.test(normalized)) {
|
|
12271
|
+
const result = /телемост/iu.test(normalized) ? await yandexTelemostStatus() : await yandexCalendarStatus();
|
|
12272
|
+
return /телемост/iu.test(normalized)
|
|
12273
|
+
? `${result.message} Календарь: ${result.calendar || "-"}.`
|
|
12274
|
+
: `Яндекс Календарь подключен: ${result.displayName || result.url}. Календарей: ${result.calendars || 1}.`;
|
|
12275
|
+
}
|
|
12276
|
+
if (/(какие|список).{0,30}(календар|календари)|календари/iu.test(normalized)) {
|
|
12277
|
+
const rows = await yandexCalendarCalendars({ limit: 20 });
|
|
12278
|
+
if (!rows.length) return "Календари Яндекса не найдены.";
|
|
12279
|
+
return ["Яндекс Календари:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.href}`)].join("\n");
|
|
12280
|
+
}
|
|
12281
|
+
if (/(напомин|уведом)/iu.test(normalized) && /(добав|постав|создай)/iu.test(normalized)) {
|
|
12282
|
+
const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
|
|
12283
|
+
const result = await yandexCalendarAddReminder(cleanupCalendarEventQuery(question), { minutes, confirm: true });
|
|
12284
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12285
|
+
}
|
|
12286
|
+
if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
|
|
12287
|
+
const dateTime = extractDateTimeFromText(question);
|
|
12288
|
+
const title = extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA");
|
|
12289
|
+
const args = { ...dateTime, title, description: extractShareMessage(question) || "", confirm: true };
|
|
12290
|
+
const result = /телемост/iu.test(normalized)
|
|
12291
|
+
? await yandexTelemostCreateEvent(args)
|
|
12292
|
+
: /кажд|еженед|ежеднев|ежемесяч|повтор/iu.test(normalized)
|
|
12293
|
+
? await yandexCalendarCreateRecurringEvent({ ...args, ...extractCalendarRepeat(question), confirm: true })
|
|
12294
|
+
: await yandexCalendarCreateEvent(args);
|
|
12295
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12296
|
+
}
|
|
12297
|
+
if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) {
|
|
12298
|
+
const result = await yandexCalendarMove(cleanupCalendarEventQuery(question), { ...extractDateTimeFromText(question), confirm: true });
|
|
12299
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12300
|
+
}
|
|
12301
|
+
if (/(переимен|измени\s+назв|смени\s+назв)/iu.test(normalized)) {
|
|
12302
|
+
const result = await yandexCalendarUpdate(cleanupCalendarEventQuery(question), { title: extractCalendarTitle(question), confirm: true });
|
|
12303
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12304
|
+
}
|
|
12305
|
+
if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) {
|
|
12306
|
+
const result = await yandexCalendarDelete(cleanupCalendarEventQuery(question), { confirm: true });
|
|
12307
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12308
|
+
}
|
|
12309
|
+
if (/(найди|поиск|где|покажи).{0,40}(событи|встреч|телемост)/iu.test(normalized)) {
|
|
12310
|
+
const rows = await yandexCalendarSearch(cleanupCalendarEventQuery(question), { limit: 20 });
|
|
12311
|
+
if (!rows.length) return "События в Яндекс Календаре по запросу не найдены.";
|
|
12312
|
+
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
|
|
12313
|
+
}
|
|
11360
12314
|
const rows = await yandexCalendarList({ limit: 10 });
|
|
11361
12315
|
if (!rows.length) return "В ближайшие дни событий в Яндекс Календаре не найдено.";
|
|
11362
|
-
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.start || "-"}`)].join("\n");
|
|
12316
|
+
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
|
|
11363
12317
|
}
|
|
11364
12318
|
|
|
11365
12319
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
@@ -11609,6 +12563,11 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
|
|
|
11609
12563
|
const result = await yandexContactCreateDiskFolder({ query, confirm: true });
|
|
11610
12564
|
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11611
12565
|
}
|
|
12566
|
+
if (/(полный|комплект|пакет|подготовь).{0,80}(контакт|клиент|человек)/iu.test(text) || /(контакт|клиент|человек).{0,80}(полный|комплект|пакет)/iu.test(text)) {
|
|
12567
|
+
const query = cleanupYandexContactActionQuery(question);
|
|
12568
|
+
const result = await yandexContactFullPack({ query, ...extractDateTimeFromText(question), note: extractShareMessage(question), confirm: true });
|
|
12569
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12570
|
+
}
|
|
11612
12571
|
if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(text)) {
|
|
11613
12572
|
const remotePath = extractCloudPath(question);
|
|
11614
12573
|
const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
|
|
@@ -11643,6 +12602,27 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
|
|
|
11643
12602
|
async function buildUserSkillDirectAnswer(question) {
|
|
11644
12603
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
11645
12604
|
if (!/(skill|скилл|скил|навык)/iu.test(normalized)) return "";
|
|
12605
|
+
if (/(шаблон|template|вариант)/iu.test(normalized) && /(покажи|список|какие|list)/iu.test(normalized)) {
|
|
12606
|
+
const rows = userSkillTemplates();
|
|
12607
|
+
return ["Шаблоны skills:", ...rows.map((row) => `- ${row.name}: ${row.description}`)].join("\n");
|
|
12608
|
+
}
|
|
12609
|
+
if (/(preview|предпросмотр|покажи\s+как)/iu.test(normalized)) {
|
|
12610
|
+
const name = extractUserSkillNameFromQuestion(question) || "user-skill";
|
|
12611
|
+
const template = normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || "");
|
|
12612
|
+
return buildUserSkillPreview({ name, template, instructions: question });
|
|
12613
|
+
}
|
|
12614
|
+
if (/(проверь|validate|doctor|валидац)/iu.test(normalized)) {
|
|
12615
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
12616
|
+
if (!name) return "Какой skill проверить? Укажите имя.";
|
|
12617
|
+
const result = await userSkillValidate(name);
|
|
12618
|
+
return ["Проверка skill:", ...result.checks.map((row) => `${row.status}: ${row.check} - ${row.message}`)].join("\n");
|
|
12619
|
+
}
|
|
12620
|
+
if (/(обнови|измени|update|edit)/iu.test(normalized)) {
|
|
12621
|
+
const name = extractUserSkillNameFromQuestion(question);
|
|
12622
|
+
if (!name) return "Какой skill обновить? Укажите имя.";
|
|
12623
|
+
const result = await userSkillUpdate(name, { instructions: question, tools: inferUserSkillTools(question), confirm: true });
|
|
12624
|
+
return `Skill обновлен: ${result.name}\nФайл: ${result.file}`;
|
|
12625
|
+
}
|
|
11646
12626
|
if (/(создай|добавь|сделай|create|new)/iu.test(normalized)) {
|
|
11647
12627
|
const name = extractUserSkillNameFromQuestion(question) || "user-skill";
|
|
11648
12628
|
const result = await userSkillCreate({
|
|
@@ -11650,6 +12630,7 @@ async function buildUserSkillDirectAnswer(question) {
|
|
|
11650
12630
|
description: extractUserSkillDescription(question, name),
|
|
11651
12631
|
instructions: question,
|
|
11652
12632
|
tools: inferUserSkillTools(question),
|
|
12633
|
+
template: normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || ""),
|
|
11653
12634
|
enable: true,
|
|
11654
12635
|
confirm: true,
|
|
11655
12636
|
});
|
|
@@ -11682,7 +12663,7 @@ async function buildUserSkillDirectAnswer(question) {
|
|
|
11682
12663
|
}
|
|
11683
12664
|
|
|
11684
12665
|
function isYandexServiceQuestion(normalized) {
|
|
11685
|
-
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex
|
|
12666
|
+
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
|
|
11686
12667
|
}
|
|
11687
12668
|
|
|
11688
12669
|
function isYandexIdentityQuestion(normalized) {
|
|
@@ -11967,6 +12948,38 @@ async function buildCloudDirectAnswer(question) {
|
|
|
11967
12948
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
11968
12949
|
try {
|
|
11969
12950
|
const provider = await getCloudProvider();
|
|
12951
|
+
if (provider === "yandex-disk" && /(документ|docs|360)/iu.test(normalized)) {
|
|
12952
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
|
|
12953
|
+
const result = await yandexDocsShare(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
|
|
12954
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12955
|
+
}
|
|
12956
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
|
|
12957
|
+
const text = extractShareMessage(question) || cleanupCloudSaveText(question);
|
|
12958
|
+
if (!text) return "Укажите текст документа.";
|
|
12959
|
+
const result = await yandexDocsCreateText({ title: extractYandexDocTitle(question), text, confirm: true });
|
|
12960
|
+
return `Документ создан на Яндекс Диске: ${result.remote}.`;
|
|
12961
|
+
}
|
|
12962
|
+
if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
|
|
12963
|
+
const result = await yandexDocsRead(extractCloudPath(question) || cleanupCloudQuery(question), {});
|
|
12964
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12965
|
+
}
|
|
12966
|
+
if (/(переимен|rename)/iu.test(normalized)) {
|
|
12967
|
+
const result = await yandexDocsRename(extractCloudPath(question) || cleanupCloudQuery(question), extractCloudNewName(question), { confirm: true });
|
|
12968
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12969
|
+
}
|
|
12970
|
+
if (/(удали|удалить)/iu.test(normalized)) {
|
|
12971
|
+
const result = await yandexDocsDelete(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
|
|
12972
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12973
|
+
}
|
|
12974
|
+
if (/(найди|поиск)/iu.test(normalized)) {
|
|
12975
|
+
const rows = await yandexDocsFind(cleanupCloudQuery(question), { limit: 20 });
|
|
12976
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
12977
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
12978
|
+
}
|
|
12979
|
+
const rows = await yandexDocsList({ limit: 20 });
|
|
12980
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
12981
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
12982
|
+
}
|
|
11970
12983
|
if (provider === "yandex-disk" && /(мест[оа]|сколько.*занято|сколько.*свобод|инфо|статус)/iu.test(normalized)) {
|
|
11971
12984
|
const info = await yandexDiskInfo();
|
|
11972
12985
|
return [
|
|
@@ -12200,6 +13213,7 @@ function extractCloudPath(question) {
|
|
|
12200
13213
|
function cleanupCloudPathCandidate(value) {
|
|
12201
13214
|
return String(value || "")
|
|
12202
13215
|
.replace(/\s+(?:по\s+почт[еуы]|на\s+почт[уые]|контакту|получател[юя]|кому|с\s+темой|тема\s*:|текст\s*:).*$/iu, "")
|
|
13216
|
+
.replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, "")
|
|
12203
13217
|
.replace(/[.!?]+$/u, "")
|
|
12204
13218
|
.trim();
|
|
12205
13219
|
}
|
|
@@ -12249,7 +13263,50 @@ function extractMailSubject(question) {
|
|
|
12249
13263
|
}
|
|
12250
13264
|
|
|
12251
13265
|
function extractShareMessage(question) {
|
|
12252
|
-
return String(question || "").match(/(
|
|
13266
|
+
return String(question || "").match(/(?:текст|text|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
|
|
13267
|
+
}
|
|
13268
|
+
|
|
13269
|
+
function extractYandexDocTitle(question) {
|
|
13270
|
+
const text = String(question || "");
|
|
13271
|
+
const raw = text.match(/(?:названи(?:е|ем)|имя|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
13272
|
+
|| text.match(/(?:документ|файл)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
13273
|
+
|| "";
|
|
13274
|
+
return raw.replace(/\s+(?:текст|text|body|сообщение)\s*:?.*$/iu, "").trim();
|
|
13275
|
+
}
|
|
13276
|
+
|
|
13277
|
+
function extractCalendarTitle(question) {
|
|
13278
|
+
const text = String(question || "");
|
|
13279
|
+
const raw = text.match(/(?:тема|названи(?:е|ем)|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
13280
|
+
|| text.match(/(?:событи[ея]|встреч[ауи]|телемост)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
13281
|
+
|| "";
|
|
13282
|
+
return raw
|
|
13283
|
+
.replace(/\s+(?:сегодня|завтра|послезавтра)(?:\s|$).*$/iu, "")
|
|
13284
|
+
.replace(/\s+(?:в\s+\d{1,2}(?::\d{2})?|на\s+\d{1,2}[.\-/]\d{1,2}[\d.\-/]*)(?:\s|$).*$/iu, "")
|
|
13285
|
+
.trim();
|
|
13286
|
+
}
|
|
13287
|
+
|
|
13288
|
+
function extractCalendarRepeat(question) {
|
|
13289
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
13290
|
+
const count = Number(text.match(/(\d+)\s*(?:раз|повтор)/iu)?.[1] || 0);
|
|
13291
|
+
return {
|
|
13292
|
+
repeat: /ежеднев|каждый\s+день/iu.test(text) ? "daily"
|
|
13293
|
+
: /ежемесяч|каждый\s+месяц/iu.test(text) ? "monthly"
|
|
13294
|
+
: /ежегод|каждый\s+год/iu.test(text) ? "yearly"
|
|
13295
|
+
: "weekly",
|
|
13296
|
+
count: count || undefined,
|
|
13297
|
+
};
|
|
13298
|
+
}
|
|
13299
|
+
|
|
13300
|
+
function cleanupCalendarEventQuery(question) {
|
|
13301
|
+
return String(question || "")
|
|
13302
|
+
.replace(/(?:создай|добавь|запланируй|назначь|перенеси|перемести|измени|смени|удали|удалить|отмени|отменить|найди|поиск|покажи|добавь\s+напоминание|поставь\s+напоминание)/giu, " ")
|
|
13303
|
+
.replace(/(?:^|\s)(?:событи\p{L}*|встреч\p{L}*|телемост\p{L}*|календар\p{L}*|напомин\p{L}*|уведом\p{L}*|яндекс|на|к|ко|в|во|сегодня|завтра|послезавтра|час|часа|часов|минут|минуты)(?=\s|$)/giu, " ")
|
|
13304
|
+
.replace(/\d{1,2}[.\-/]\d{1,2}(?:[.\-/]\d{2,4})?/gu, " ")
|
|
13305
|
+
.replace(/\d{1,2}[:.]\d{2}/gu, " ")
|
|
13306
|
+
.replace(/(?:^|\s)\d{1,3}(?=\s|$)/gu, " ")
|
|
13307
|
+
.replace(/[,:;.!?«»"()]+/gu, " ")
|
|
13308
|
+
.replace(/\s+/g, " ")
|
|
13309
|
+
.trim();
|
|
12253
13310
|
}
|
|
12254
13311
|
|
|
12255
13312
|
function parseYandexDiskPackageRequest(question) {
|
|
@@ -12826,9 +13883,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
12826
13883
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
12827
13884
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
12828
13885
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
12829
|
-
"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_list {start,end}, yandex_calendar_create_event {title,start,end,location,attendees,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}.",
|
|
12830
|
-
"Опасные 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_telemost_create_event.",
|
|
12831
|
-
"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.",
|
|
13886
|
+
"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_daily_digest {save,email}, yandex_calendar_reminders_tick {}, yandex_disk_maintenance_tick {}.",
|
|
13887
|
+
"Опасные 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.",
|
|
13888
|
+
"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.",
|
|
12832
13889
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
12833
13890
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
12834
13891
|
`Вопрос: ${question}`,
|
|
@@ -12908,6 +13965,12 @@ function inferToolPlan(question, options = {}) {
|
|
|
12908
13965
|
}],
|
|
12909
13966
|
};
|
|
12910
13967
|
}
|
|
13968
|
+
if (/(шаблон|template)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
|
|
13969
|
+
return { steps: [{ tool: "user_skill_templates", args: {} }] };
|
|
13970
|
+
}
|
|
13971
|
+
if (/(проверь|validate|doctor)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
|
|
13972
|
+
return { steps: [{ tool: "user_skill_validate", args: { name: extractUserSkillNameFromQuestion(question) } }] };
|
|
13973
|
+
}
|
|
12911
13974
|
if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
|
|
12912
13975
|
return { steps: [{ tool: "yandex_identity_me", args: {} }] };
|
|
12913
13976
|
}
|
|
@@ -12938,12 +14001,31 @@ function inferToolPlan(question, options = {}) {
|
|
|
12938
14001
|
if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_reply", args: { uid, mailbox, text: parseYandexMailReplyRequest(question).text, confirm: true } }] };
|
|
12939
14002
|
if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_delete", args: { uid, mailbox, confirm: true } }] };
|
|
12940
14003
|
if (/(пометь|отметь|сделай)/iu.test(normalized) && /(прочитан|непрочитан)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_mark", args: { uid, mailbox, seen: !/непрочитан/iu.test(normalized) } }] };
|
|
14004
|
+
if (/(пакет|комплект)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_meeting_pack", args: { uid, mailbox, ...extractDateTimeFromText(question), confirm: true } }] };
|
|
12941
14005
|
if (/(прочитай|прочти|открой|раскрой|получи|получить)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_read", args: { uid, mailbox } }] };
|
|
12942
14006
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
|
|
12943
14007
|
return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
|
|
12944
14008
|
}
|
|
14009
|
+
if (/(документ|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
|
|
14010
|
+
const target = extractCloudPath(question) || cleanupYandexQuery(question);
|
|
14011
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_create_text", args: { title: extractYandexDocTitle(question), text: extractShareMessage(question) || cleanupCloudSaveText(question), confirm: true } }] };
|
|
14012
|
+
if (/(прочитай|открой|текст)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_read", args: { query: target } }] };
|
|
14013
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_share", args: { query: target, confirm: true } }] };
|
|
14014
|
+
if (/(переимен|rename)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_rename", args: { query: target, name: extractCloudNewName(question), confirm: true } }] };
|
|
14015
|
+
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_delete", args: { query: target, confirm: true } }] };
|
|
14016
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_find", args: { query: cleanupYandexQuery(question), limit: 20 } }] };
|
|
14017
|
+
return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
|
|
14018
|
+
}
|
|
12945
14019
|
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
12946
|
-
return { steps: [{ tool:
|
|
14020
|
+
if (/(напомин|уведом|следи|монитор)/iu.test(normalized) && /(проверь|tick|сейчас)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_reminders_tick", args: { force: true } }] };
|
|
14021
|
+
if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
|
|
14022
|
+
const dateTime = extractDateTimeFromText(question);
|
|
14023
|
+
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 } }] };
|
|
14024
|
+
}
|
|
14025
|
+
if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_move", args: { query: cleanupCalendarEventQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
|
|
14026
|
+
if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_delete", args: { query: cleanupCalendarEventQuery(question), confirm: true } }] };
|
|
14027
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_search", args: { query: cleanupCalendarEventQuery(question), limit: 20 } }] };
|
|
14028
|
+
return { steps: [{ tool: "yandex_calendar_list", args: { limit: 20 } }] };
|
|
12947
14029
|
}
|
|
12948
14030
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
12949
14031
|
if (/(дубликат|повтор)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_duplicates", args: { limit: 20 } }] };
|
|
@@ -12951,6 +14033,7 @@ function inferToolPlan(question, options = {}) {
|
|
|
12951
14033
|
if (/(экспорт|выгруз)/iu.test(normalized)) return { steps: [{ tool: /csv/iu.test(normalized) ? "yandex_contacts_export_csv" : "yandex_contacts_export_vcard", args: {} }] };
|
|
12952
14034
|
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 } }] };
|
|
12953
14035
|
if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_create", args: { ...parseYandexContactCreateRequest(question), confirm: true } }] };
|
|
14036
|
+
if (/(полный|комплект|пакет)/iu.test(normalized)) return { steps: [{ tool: "yandex_contact_full_pack", args: { contact: cleanupYandexContactActionQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
|
|
12954
14037
|
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_delete", args: { query: cleanupYandexContactActionQuery(question), confirm: true } }] };
|
|
12955
14038
|
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 } }] };
|
|
12956
14039
|
if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
|
|
@@ -13537,6 +14620,21 @@ function formatToolResult(result, options) {
|
|
|
13537
14620
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
13538
14621
|
}
|
|
13539
14622
|
if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
|
|
14623
|
+
if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
|
|
14624
|
+
return `${row.status === "calendar-event-created" ? "Событие" : "Телемост"} создан: ${row.title || row.uid}${row.start ? `, ${row.start}` : ""}${row.telemost?.joinUrl ? `\nСсылка: ${row.telemost.joinUrl}` : row.status === "telemost-calendar-fallback-created" ? "\nПрямая ссылка Телемоста через API недоступна, создано событие календаря." : ""}`;
|
|
14625
|
+
}
|
|
14626
|
+
if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
|
|
14627
|
+
if (row.status === "calendar-event-deleted") return `Событие удалено: ${row.title || row.uid}`;
|
|
14628
|
+
if (row.status === "mail-meeting-pack-created") return `Пакет по письму #${row.uid} создан.\nПисьмо: ${row.saved}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}\nСобытие: ${row.event || "-"}`;
|
|
14629
|
+
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 || "-"}`;
|
|
14630
|
+
if (row.enabled && row.text && (row.unread !== undefined || row.events !== undefined)) return row.text;
|
|
14631
|
+
if (row.status === "ambiguous" && row.events) return [`Нашел несколько событий. Уточните:`, ...row.events.map((event, index) => `${index + 1}. ${event.title || event.uid} — ${event.startIso || event.start || "-"}`)].join("\n");
|
|
14632
|
+
if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
|
|
14633
|
+
if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
|
|
14634
|
+
if (row.status === "binary-document") return `${row.name || row.remote}: ${row.message}`;
|
|
14635
|
+
if (row.status === "not-document") return `Это не документ: ${row.path}`;
|
|
14636
|
+
if (row.status === "ambiguous" && row.docs) return [`Нашел несколько документов. Уточните:`, ...row.docs.map((doc, index) => `${index + 1}. ${doc.name} — ${doc.path}`)].join("\n");
|
|
14637
|
+
if (row.status === "not-found" && row.docs !== undefined) return `Документ не найден: ${row.query}`;
|
|
13540
14638
|
if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
|
|
13541
14639
|
if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
13542
14640
|
if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
|
|
@@ -15094,12 +16192,12 @@ function parseOptions(args) {
|
|
|
15094
16192
|
|
|
15095
16193
|
for (let index = 0; index < args.length; index += 1) {
|
|
15096
16194
|
const arg = args[index];
|
|
15097
|
-
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") {
|
|
16195
|
+
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") {
|
|
15098
16196
|
result[arg.slice(2)] = true;
|
|
15099
16197
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
15100
16198
|
result.check = true;
|
|
15101
16199
|
result[arg.slice(2)] = true;
|
|
15102
|
-
} 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") {
|
|
16200
|
+
} 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") {
|
|
15103
16201
|
result[arg.slice(2)] = args[index + 1];
|
|
15104
16202
|
index += 1;
|
|
15105
16203
|
} else {
|
|
@@ -15323,9 +16421,13 @@ function printSkillsList(skills, config) {
|
|
|
15323
16421
|
async function executeUserSkillTool(tool, args = {}) {
|
|
15324
16422
|
if (tool === "user_skill_list") return listSkills(await loadConfig()).filter((skill) => skill.source === "user");
|
|
15325
16423
|
if (tool === "user_skill_create") return userSkillCreate({ ...args, confirm: args.confirm === true });
|
|
16424
|
+
if (tool === "user_skill_update") return userSkillUpdate(args.name, { ...args, confirm: args.confirm === true });
|
|
15326
16425
|
if (tool === "user_skill_enable") return userSkillSetEnabled(args.name, true);
|
|
15327
16426
|
if (tool === "user_skill_disable") return userSkillSetEnabled(args.name, false);
|
|
15328
16427
|
if (tool === "user_skill_delete") return userSkillDelete(args.name, { confirm: args.confirm === true });
|
|
16428
|
+
if (tool === "user_skill_templates") return userSkillTemplates();
|
|
16429
|
+
if (tool === "user_skill_validate") return userSkillValidate(args.name);
|
|
16430
|
+
if (tool === "user_skill_preview") return { status: "preview", text: buildUserSkillPreview(args) };
|
|
15329
16431
|
throw new Error(`Неизвестный user skill tool: ${tool}`);
|
|
15330
16432
|
}
|
|
15331
16433
|
|
|
@@ -15333,22 +16435,41 @@ async function userSkillCreate(args = {}) {
|
|
|
15333
16435
|
if (!args.confirm) throw new Error("Для создания пользовательского skill нужен аргумент confirm=true.");
|
|
15334
16436
|
const name = normalizeUserSkillName(args.name || args.skill || args.title);
|
|
15335
16437
|
if (!name) throw new Error("Имя skill обязательно.");
|
|
15336
|
-
const
|
|
15337
|
-
const
|
|
16438
|
+
const template = getUserSkillTemplate(args.template);
|
|
16439
|
+
const description = String(args.description || template?.description || `Пользовательский skill: ${name}`).trim();
|
|
16440
|
+
const instructions = String(args.instructions || args.text || args.prompt || template?.instructions || "").trim();
|
|
15338
16441
|
if (!instructions) throw new Error("Инструкции skill обязательны.");
|
|
15339
16442
|
const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || "");
|
|
16443
|
+
const mergedTools = [...new Set([...(template?.tools || []), ...tools])];
|
|
15340
16444
|
const dir = path.join(USER_SKILLS_DIR, name);
|
|
15341
16445
|
const file = path.join(dir, "SKILL.md");
|
|
15342
16446
|
if (existsSync(file) && !args.overwrite) throw new Error(`Skill уже существует: ${name}. Используйте overwrite=true или --force.`);
|
|
15343
16447
|
await mkdir(dir, { recursive: true });
|
|
15344
|
-
const body = buildUserSkillMarkdown({ name, description, instructions, tools });
|
|
16448
|
+
const body = buildUserSkillMarkdown({ name, description, instructions, tools: mergedTools });
|
|
15345
16449
|
await writeFile(file, body, "utf8");
|
|
15346
16450
|
let enabled = false;
|
|
15347
16451
|
if (args.enable) {
|
|
15348
16452
|
await userSkillSetEnabled(name, true);
|
|
15349
16453
|
enabled = true;
|
|
15350
16454
|
}
|
|
15351
|
-
return { name, description, file, enabled, tools, status: "created" };
|
|
16455
|
+
return { name, description, file, enabled, tools: mergedTools, status: "created" };
|
|
16456
|
+
}
|
|
16457
|
+
|
|
16458
|
+
async function userSkillUpdate(name, args = {}) {
|
|
16459
|
+
if (!args.confirm) throw new Error("Для обновления пользовательского skill нужен confirm=true.");
|
|
16460
|
+
const skillName = normalizeUserSkillName(name || args.name || args.skill);
|
|
16461
|
+
if (!skillName) throw new Error("Имя skill обязательно.");
|
|
16462
|
+
const file = path.join(USER_SKILLS_DIR, skillName, "SKILL.md");
|
|
16463
|
+
if (!existsSync(file)) throw new Error(`Пользовательский skill не найден: ${skillName}`);
|
|
16464
|
+
const current = await readFile(file, "utf8");
|
|
16465
|
+
const meta = readSkillMeta(file);
|
|
16466
|
+
const description = String(args.description || meta.description || `Пользовательский skill: ${skillName}`).trim();
|
|
16467
|
+
const instructions = String(args.instructions || args.text || args.prompt || stripFrontmatter(current)).trim();
|
|
16468
|
+
const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions));
|
|
16469
|
+
const body = buildUserSkillMarkdown({ name: skillName, description, instructions, tools });
|
|
16470
|
+
await writeFile(file, body, "utf8");
|
|
16471
|
+
if (args.enable) await userSkillSetEnabled(skillName, true);
|
|
16472
|
+
return { name: skillName, description, file, tools, status: "updated" };
|
|
15352
16473
|
}
|
|
15353
16474
|
|
|
15354
16475
|
async function userSkillSetEnabled(name, enabled) {
|
|
@@ -15442,9 +16563,23 @@ function inferUserSkillTools(question) {
|
|
|
15442
16563
|
}
|
|
15443
16564
|
if (/(календар|событи|встреч|телемост)/iu.test(text)) {
|
|
15444
16565
|
tools.add("yandex_calendar_list");
|
|
16566
|
+
tools.add("yandex_calendar_search");
|
|
15445
16567
|
tools.add("yandex_calendar_create_event");
|
|
16568
|
+
tools.add("yandex_calendar_update");
|
|
16569
|
+
tools.add("yandex_calendar_move");
|
|
16570
|
+
tools.add("yandex_calendar_delete");
|
|
16571
|
+
tools.add("yandex_calendar_add_reminder");
|
|
15446
16572
|
tools.add("yandex_telemost_create_event");
|
|
15447
16573
|
}
|
|
16574
|
+
if (/(документ|docs|360)/iu.test(text)) {
|
|
16575
|
+
tools.add("yandex_docs_list");
|
|
16576
|
+
tools.add("yandex_docs_find");
|
|
16577
|
+
tools.add("yandex_docs_create_text");
|
|
16578
|
+
tools.add("yandex_docs_read");
|
|
16579
|
+
tools.add("yandex_docs_share");
|
|
16580
|
+
tools.add("yandex_docs_rename");
|
|
16581
|
+
tools.add("yandex_docs_delete");
|
|
16582
|
+
}
|
|
15448
16583
|
if (/(контакт|адресн)/iu.test(text)) {
|
|
15449
16584
|
tools.add("yandex_contacts_search");
|
|
15450
16585
|
tools.add("yandex_contacts_get");
|
|
@@ -15485,6 +16620,69 @@ function inferUserSkillTools(question) {
|
|
|
15485
16620
|
return [...tools];
|
|
15486
16621
|
}
|
|
15487
16622
|
|
|
16623
|
+
function userSkillTemplates() {
|
|
16624
|
+
return [
|
|
16625
|
+
{
|
|
16626
|
+
name: "mail-triage",
|
|
16627
|
+
description: "Разбор почты: найти важные письма, прочитать, сохранить, создать задачу или событие.",
|
|
16628
|
+
tools: ["yandex_mail_list", "yandex_mail_search", "yandex_mail_read", "yandex_mail_save_to_disk", "yandex_mail_create_calendar_event", "yandex_mail_create_task"],
|
|
16629
|
+
instructions: "Помогай разбирать почту пользователя. Сначала показывай краткую сводку, затем выполняй только явно запрошенные действия: чтение, сохранение письма на Диск, создание события или задачи.",
|
|
16630
|
+
},
|
|
16631
|
+
{
|
|
16632
|
+
name: "family-calendar",
|
|
16633
|
+
description: "Семейный календарь: события, напоминания, встречи и ежедневные проверки.",
|
|
16634
|
+
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"],
|
|
16635
|
+
instructions: "Помогай вести личный календарь. Для изменения календаря требуй явную просьбу пользователя. Всегда называй дату и время события.",
|
|
16636
|
+
},
|
|
16637
|
+
{
|
|
16638
|
+
name: "docs-organizer",
|
|
16639
|
+
description: "Организация документов на Яндекс Диске.",
|
|
16640
|
+
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"],
|
|
16641
|
+
instructions: "Помогай искать, создавать и приводить в порядок документы на Яндекс Диске. Не путай облачные документы с локальными файлами на компьютере.",
|
|
16642
|
+
},
|
|
16643
|
+
{
|
|
16644
|
+
name: "contact-workflow",
|
|
16645
|
+
description: "Работа с контактами, письмами, встречами и папками контактов.",
|
|
16646
|
+
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"],
|
|
16647
|
+
instructions: "Помогай выполнять действия вокруг контакта: найти карточку, отправить письмо, создать папку, отправить ссылку или создать встречу. Если найдено несколько контактов, проси уточнение.",
|
|
16648
|
+
},
|
|
16649
|
+
];
|
|
16650
|
+
}
|
|
16651
|
+
|
|
16652
|
+
function getUserSkillTemplate(name) {
|
|
16653
|
+
const normalized = normalizeUserSkillName(name || "");
|
|
16654
|
+
return userSkillTemplates().find((item) => item.name === normalized) || null;
|
|
16655
|
+
}
|
|
16656
|
+
|
|
16657
|
+
function buildUserSkillPreview(args = {}) {
|
|
16658
|
+
const name = normalizeUserSkillName(args.name || args.skill || args.title || "user-skill");
|
|
16659
|
+
const template = getUserSkillTemplate(args.template);
|
|
16660
|
+
const description = args.description || template?.description || `Пользовательский skill: ${name}`;
|
|
16661
|
+
const instructions = args.instructions || args.text || args.prompt || template?.instructions || "Опишите, что должен делать skill.";
|
|
16662
|
+
const tools = [...new Set([...(template?.tools || []), ...parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions))])];
|
|
16663
|
+
return buildUserSkillMarkdown({ name, description, instructions, tools });
|
|
16664
|
+
}
|
|
16665
|
+
|
|
16666
|
+
async function userSkillValidate(name) {
|
|
16667
|
+
const config = await loadConfig();
|
|
16668
|
+
const skill = findSkill(normalizeUserSkillName(name), config);
|
|
16669
|
+
const checks = [];
|
|
16670
|
+
if (!skill) {
|
|
16671
|
+
return { status: "error", checks: [{ check: "exists", status: "error", message: `Skill не найден: ${name || "-"}` }] };
|
|
16672
|
+
}
|
|
16673
|
+
const text = await readFile(skill.file, "utf8");
|
|
16674
|
+
const meta = readSkillMeta(skill.file);
|
|
16675
|
+
checks.push({ check: "exists", status: "ok", message: skill.file });
|
|
16676
|
+
checks.push({ check: "name", status: meta.name ? "ok" : "error", message: meta.name || "Нет name во frontmatter" });
|
|
16677
|
+
checks.push({ check: "description", status: meta.description ? "ok" : "warn", message: meta.description || "Описание пустое" });
|
|
16678
|
+
checks.push({ check: "instructions", status: stripFrontmatter(text).trim().length >= 40 ? "ok" : "warn", message: "Инструкции должны быть понятными и достаточно подробными" });
|
|
16679
|
+
checks.push({ check: "secrets", status: /(token|api[_-]?key|парол|секрет)\s*[:=]\s*\S{8,}/iu.test(text) ? "error" : "ok", message: "Skill не должен содержать секреты" });
|
|
16680
|
+
const tools = [...text.matchAll(/`([a-z0-9_:-]+)`/giu)].map((match) => match[1]).filter((tool) => ALL_TOOL_ALIASES.includes(tool) || tool.startsWith("mcp:"));
|
|
16681
|
+
const unknownTools = tools.filter((tool) => !ALL_TOOL_ALIASES.includes(tool) && !tool.startsWith("mcp:"));
|
|
16682
|
+
checks.push({ check: "tools", status: unknownTools.length ? "warn" : "ok", message: unknownTools.length ? `Неизвестные tools: ${unknownTools.join(", ")}` : `${tools.length} tools` });
|
|
16683
|
+
return { status: checks.some((row) => row.status === "error") ? "error" : "ok", checks };
|
|
16684
|
+
}
|
|
16685
|
+
|
|
15488
16686
|
function buildUserSkillMarkdown({ name, description, instructions, tools = [] }) {
|
|
15489
16687
|
const toolLines = tools.length
|
|
15490
16688
|
? ["", "Разрешенные/ожидаемые tools для этого skill:", "", ...tools.map((tool) => `- \`${tool}\``)]
|
|
@@ -15554,7 +16752,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
15554
16752
|
if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
|
|
15555
16753
|
const cloudQuestion = isCloudQuestion(normalized);
|
|
15556
16754
|
if (enabled.has("personal-docs") && cloudQuestion) selected.add("personal-docs");
|
|
15557
|
-
if (enabled.has("yandex-services") && /(
|
|
16755
|
+
if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|документ|docs|360|облако)/iu.test(normalized)) selected.add("yandex-services");
|
|
15558
16756
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
15559
16757
|
if (enabled.has("local-files") && !cloudQuestion && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
15560
16758
|
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|