@iola_adm/iola-cli 0.2.34 → 0.2.35
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 +1 -1
- package/package.json +1 -1
- package/skills/yandex-services/SKILL.md +25 -4
- package/src/cli.js +649 -31
- package/wiki/Skills-/320/270-toolsets.md +4 -3
- package/wiki/Yandex-Connector.md +20 -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 +48 -2
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ Yandex Connector использует две встроенные OAuth-груп
|
|
|
202
202
|
|
|
203
203
|
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. Мастер считает коннектор готовым только после токенов обеих групп.
|
|
204
204
|
|
|
205
|
-
Yandex tools уже доступны: профиль Yandex ID, расширенная работа с Яндекс Диском (место, список, поиск, карточка, чтение текста, папки, загрузка, скачивание, ссылки, QR-коды к публичным ссылкам, отправка ссылки и QR по почте, перемещение, копирование, переименование, корзина), статус/список/поиск/чтение/отправка Яндекс Почты,
|
|
205
|
+
Yandex tools уже доступны: профиль Yandex ID, расширенная работа с Яндекс Диском (место, список, поиск, карточка, чтение текста, папки, загрузка, скачивание, ссылки, QR-коды к публичным ссылкам, отправка ссылки и QR по почте, перемещение, копирование, переименование, корзина), статус/список/поиск/чтение/отправка Яндекс Почты, полноценная работа с Календарем через CalDAV (календари, список, поиск, создание, перенос, редактирование, напоминания, повторы, удаление), Яндекс Документы/360 через Диск (создание текстовых документов, поиск, чтение, ссылки/QR, переименование, удаление), расширенные Яндекс Контакты (поиск, создание, обновление, удаление, импорт/экспорт, дубликаты, backup на Диск, дни рождения в календарь, регулярная contacts-maintenance проверка) и комбинированные сценарии: письмо контакту, ссылка+QR контакту, папка контакта на Диске, встреча/Телемост с контактом. Телемост пытается использовать прямой API, а если он недоступен текущему аккаунту, честно создает календарное событие без выдуманной ссылки. Отправка письма, удаление/перемещение файлов, публикация ссылок, изменение контактов, документов и событий требуют явного подтверждения.
|
|
206
206
|
|
|
207
207
|
Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
|
|
208
208
|
|
package/package.json
CHANGED
|
@@ -50,8 +50,25 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
50
50
|
- `yandex_mail_create_task` - создать локальную задачу по письму.
|
|
51
51
|
- Автоопрос почты включается командой `iola yandex mail-watch on --minutes 5`, выключается `iola yandex mail-watch off`, ручная проверка `iola yandex mail-watch tick`.
|
|
52
52
|
- `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
|
|
53
|
-
- `
|
|
54
|
-
- `
|
|
53
|
+
- `yandex_calendar_calendars` - показать доступные календари.
|
|
54
|
+
- `yandex_calendar_list` - показать ближайшие события.
|
|
55
|
+
- `yandex_calendar_search` - найти событие по названию, описанию, месту, UID или дате.
|
|
56
|
+
- `yandex_calendar_get` - открыть карточку события.
|
|
57
|
+
- `yandex_calendar_create_event` - создать событие с участниками, местом и напоминаниями.
|
|
58
|
+
- `yandex_calendar_create_recurring_event` - создать повторяющееся событие.
|
|
59
|
+
- `yandex_calendar_update` - изменить название, описание, место, время, участников, повторы или напоминания.
|
|
60
|
+
- `yandex_calendar_move` - перенести событие на новую дату/время.
|
|
61
|
+
- `yandex_calendar_add_reminder` - добавить напоминание к событию.
|
|
62
|
+
- `yandex_calendar_delete` - удалить событие.
|
|
63
|
+
- `yandex_docs_status` - проверить работу Яндекс Документов / 360 через Диск.
|
|
64
|
+
- `yandex_docs_list` - показать документы на Яндекс Диске.
|
|
65
|
+
- `yandex_docs_find` - найти документ по названию.
|
|
66
|
+
- `yandex_docs_create_text` - создать текстовый/Markdown/HTML-документ на Яндекс Диске.
|
|
67
|
+
- `yandex_docs_read` - прочитать небольшой текстовый документ.
|
|
68
|
+
- `yandex_docs_share` - создать публичную ссылку и QR-код на документ.
|
|
69
|
+
- `yandex_docs_rename` - переименовать документ.
|
|
70
|
+
- `yandex_docs_delete` - удалить документ.
|
|
71
|
+
- `yandex_docs_save_answer` - сохранить текст ответа как документ.
|
|
55
72
|
- `yandex_contacts_status` - проверить доступ к Яндекс Контактам.
|
|
56
73
|
- `yandex_contacts_list` - показать контакты.
|
|
57
74
|
- `yandex_contacts_search` - найти контакт по имени, email, телефону, организации, адресу или заметке.
|
|
@@ -81,7 +98,8 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
81
98
|
- `yandex_contact_create_calendar_event` - создать встречу с контактом.
|
|
82
99
|
- `yandex_contact_create_telemost_event` - создать календарное событие для Телемоста с контактом.
|
|
83
100
|
- `yandex_contact_from_public_entity` - создать контакт из открытого городского слоя, если у организации есть email или телефон.
|
|
84
|
-
- `
|
|
101
|
+
- `yandex_telemost_status` - проверить режим Телемоста.
|
|
102
|
+
- `yandex_telemost_create_event` - создать встречу: если прямой Telemost API доступен аккаунту, добавить ссылку; иначе создать календарное событие с честным fallback.
|
|
85
103
|
- Регулярная проверка контактов включается командой `iola yandex contacts-maintenance on --days 7`, выключается `iola yandex contacts-maintenance off`, ручная проверка `iola yandex contacts-maintenance tick`. Флаг `--backup` включает backup контактов на Диск при tick.
|
|
86
104
|
|
|
87
105
|
Комбинированные сценарии:
|
|
@@ -92,6 +110,9 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
92
110
|
- Если получатель указан именем, сначала ищи его в контактах. Если контактов несколько или email не найден, попроси уточнение.
|
|
93
111
|
- Если пользователь просит отправить письмо контакту, создать встречу с контактом или отправить ссылку контакту, используй специализированные `yandex_contact_*` tools, а не ручную цепочку из нескольких tools.
|
|
94
112
|
- Если пользователь просит создать папку для контакта на Яндекс Диске, используй `yandex_contact_create_disk_folder`.
|
|
113
|
+
- Если пользователь просит создать/найти/прочитать/переименовать/удалить документ именно на Яндекс Диске или в Яндекс 360, используй `yandex_docs_*`, а не общий список файлов.
|
|
114
|
+
- Если пользователь просит перенести событие, добавить напоминание или удалить встречу, используй `yandex_calendar_move`, `yandex_calendar_add_reminder`, `yandex_calendar_delete`.
|
|
115
|
+
- Если пользователь просит Телемост, используй `yandex_telemost_create_event`. Не обещай ссылку Телемоста, если API ее не вернул.
|
|
95
116
|
- Если пользователь просит резервную копию контактов, используй `yandex_contacts_backup_to_disk`.
|
|
96
117
|
- Если пользователь просит регулярную проверку контактов, дубликатов или неполных карточек, используй `iola yandex contacts-maintenance ...`.
|
|
97
118
|
- Если пользователь просит импорт/экспорт контактов, используй CSV или vCard по расширению файла или явному формату.
|
|
@@ -100,7 +121,7 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
100
121
|
|
|
101
122
|
Безопасность:
|
|
102
123
|
|
|
103
|
-
- Для отправки письма, ответа на письмо, удаления письма, удаления файлов, перемещения/копирования/переименования объектов, восстановления/очистки корзины, публикации ссылки, создания QR-ссылки, отправки ссылки по
|
|
124
|
+
- Для отправки письма, ответа на письмо, удаления письма, удаления файлов, перемещения/копирования/переименования объектов, восстановления/очистки корзины, публикации ссылки, создания QR-ссылки, отправки ссылки по почте, создания/изменения/удаления событий и создания/изменения/удаления документов нужен явный запрос пользователя и `confirm=true`.
|
|
104
125
|
- Для пересылки письма, создания контакта, создания события из письма и добавления отправителя в контакты также нужен явный запрос пользователя и `confirm=true`.
|
|
105
126
|
- Не отправляй письма, не отвечай на письма, не пересылай письма, не создавай контакты/события, не удаляй письма/файлы, не перемещай/копируй/переименовывай объекты и не публикуй ссылки по косвенному намерению.
|
|
106
127
|
- Список писем и поиск не должны помечать письма прочитанными. Чтение письма по просьбе пользователя помечает письмо прочитанным.
|
package/src/cli.js
CHANGED
|
@@ -193,8 +193,25 @@ const YANDEX_TOOLS = [
|
|
|
193
193
|
"yandex_mail_map_addresses",
|
|
194
194
|
"yandex_mail_create_task",
|
|
195
195
|
"yandex_calendar_status",
|
|
196
|
+
"yandex_calendar_calendars",
|
|
196
197
|
"yandex_calendar_create_event",
|
|
197
198
|
"yandex_calendar_list",
|
|
199
|
+
"yandex_calendar_get",
|
|
200
|
+
"yandex_calendar_search",
|
|
201
|
+
"yandex_calendar_update",
|
|
202
|
+
"yandex_calendar_move",
|
|
203
|
+
"yandex_calendar_delete",
|
|
204
|
+
"yandex_calendar_create_recurring_event",
|
|
205
|
+
"yandex_calendar_add_reminder",
|
|
206
|
+
"yandex_docs_status",
|
|
207
|
+
"yandex_docs_list",
|
|
208
|
+
"yandex_docs_find",
|
|
209
|
+
"yandex_docs_create_text",
|
|
210
|
+
"yandex_docs_read",
|
|
211
|
+
"yandex_docs_share",
|
|
212
|
+
"yandex_docs_rename",
|
|
213
|
+
"yandex_docs_delete",
|
|
214
|
+
"yandex_docs_save_answer",
|
|
198
215
|
"yandex_contacts_status",
|
|
199
216
|
"yandex_contacts_list",
|
|
200
217
|
"yandex_contacts_search",
|
|
@@ -224,6 +241,7 @@ const YANDEX_TOOLS = [
|
|
|
224
241
|
"yandex_contact_create_calendar_event",
|
|
225
242
|
"yandex_contact_create_telemost_event",
|
|
226
243
|
"yandex_contact_from_public_entity",
|
|
244
|
+
"yandex_telemost_status",
|
|
227
245
|
"yandex_telemost_create_event",
|
|
228
246
|
];
|
|
229
247
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
|
|
@@ -4147,8 +4165,25 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4147
4165
|
if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
|
|
4148
4166
|
if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
|
|
4149
4167
|
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
4168
|
+
if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
|
|
4150
4169
|
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
4151
4170
|
if (tool === "yandex_calendar_list") return yandexCalendarList(args);
|
|
4171
|
+
if (tool === "yandex_calendar_get") return yandexCalendarGet(args.query || args.uid || args.title || "", args);
|
|
4172
|
+
if (tool === "yandex_calendar_search") return yandexCalendarSearch(args.query || args.title || "", args);
|
|
4173
|
+
if (tool === "yandex_calendar_update") return yandexCalendarUpdate(args.query || args.uid || args.title || "", args);
|
|
4174
|
+
if (tool === "yandex_calendar_move") return yandexCalendarMove(args.query || args.uid || args.title || "", args);
|
|
4175
|
+
if (tool === "yandex_calendar_delete") return yandexCalendarDelete(args.query || args.uid || args.title || "", args);
|
|
4176
|
+
if (tool === "yandex_calendar_create_recurring_event") return yandexCalendarCreateRecurringEvent(args);
|
|
4177
|
+
if (tool === "yandex_calendar_add_reminder") return yandexCalendarAddReminder(args.query || args.uid || args.title || "", args);
|
|
4178
|
+
if (tool === "yandex_docs_status") return yandexDocsStatus();
|
|
4179
|
+
if (tool === "yandex_docs_list") return yandexDocsList(args);
|
|
4180
|
+
if (tool === "yandex_docs_find") return yandexDocsFind(args.query || args.name || "", args);
|
|
4181
|
+
if (tool === "yandex_docs_create_text") return yandexDocsCreateText(args);
|
|
4182
|
+
if (tool === "yandex_docs_read") return yandexDocsRead(args.path || args.remotePath || args.query || args.name, args);
|
|
4183
|
+
if (tool === "yandex_docs_share") return yandexDocsShare(args.path || args.remotePath || args.query || args.name, args);
|
|
4184
|
+
if (tool === "yandex_docs_rename") return yandexDocsRename(args.path || args.remotePath || args.query || args.name, args.name || args.newName || args.to, args);
|
|
4185
|
+
if (tool === "yandex_docs_delete") return yandexDocsDelete(args.path || args.remotePath || args.query || args.name, args);
|
|
4186
|
+
if (tool === "yandex_docs_save_answer") return yandexDocsCreateText({ ...args, text: args.text || args.answer || args.content });
|
|
4152
4187
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
4153
4188
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
4154
4189
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
@@ -4178,6 +4213,7 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4178
4213
|
if (tool === "yandex_contact_create_calendar_event") return yandexContactCreateCalendarEvent(args);
|
|
4179
4214
|
if (tool === "yandex_contact_create_telemost_event") return yandexContactCreateTelemostEvent(args);
|
|
4180
4215
|
if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
|
|
4216
|
+
if (tool === "yandex_telemost_status") return yandexTelemostStatus();
|
|
4181
4217
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
4182
4218
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
4183
4219
|
}
|
|
@@ -4980,29 +5016,53 @@ async function yandexDavRequest(url, token, options = {}) {
|
|
|
4980
5016
|
|
|
4981
5017
|
async function yandexCalendarStatus() {
|
|
4982
5018
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4983
|
-
const
|
|
5019
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5020
|
+
const selected = pickYandexCalendar(calendars, {});
|
|
5021
|
+
const url = selected.url;
|
|
4984
5022
|
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" };
|
|
5023
|
+
return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || selected.name || "calendar", calendars: calendars.length };
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
async function yandexCalendarCalendars(args = {}) {
|
|
5027
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5028
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5029
|
+
return calendars.slice(0, Number(args.limit || 50));
|
|
4986
5030
|
}
|
|
4987
5031
|
|
|
4988
5032
|
async function yandexCalendarCreateEvent(args = {}) {
|
|
4989
5033
|
if (!args.confirm) throw new Error("Для создания события в календаре нужен аргумент confirm=true.");
|
|
4990
5034
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
4991
|
-
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
5035
|
+
const baseUrl = await yandexCalendarBaseUrl(token, args);
|
|
4992
5036
|
const uid = `${randomUUID()}@iola-cli`;
|
|
4993
5037
|
const start = toIcsDate(args.start || args.date || new Date(Date.now() + 3600000).toISOString());
|
|
4994
5038
|
const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
|
|
4995
5039
|
const summary = args.title || args.summary || "Событие IOLA";
|
|
4996
5040
|
const description = args.description || "";
|
|
4997
|
-
const ics = buildIcsEvent({
|
|
5041
|
+
const ics = buildIcsEvent({
|
|
5042
|
+
uid,
|
|
5043
|
+
start,
|
|
5044
|
+
end,
|
|
5045
|
+
summary,
|
|
5046
|
+
description,
|
|
5047
|
+
location: args.location || "",
|
|
5048
|
+
attendees: args.attendees || args.to || [],
|
|
5049
|
+
rrule: args.rrule || buildIcsRrule(args),
|
|
5050
|
+
reminders: normalizeCalendarReminders(args.reminders || args.reminder || args.alarm),
|
|
5051
|
+
});
|
|
4998
5052
|
const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
|
|
4999
5053
|
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 };
|
|
5054
|
+
return { status: "calendar-event-created", uid, title: summary, start: args.start || args.date || "", end: args.end || "", url };
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
async function yandexCalendarCreateRecurringEvent(args = {}) {
|
|
5058
|
+
if (!args.confirm) throw new Error("Для создания повторяющегося события нужен аргумент confirm=true.");
|
|
5059
|
+
const rrule = args.rrule || buildIcsRrule({ ...args, repeat: args.repeat || args.frequency || "weekly" });
|
|
5060
|
+
return yandexCalendarCreateEvent({ ...args, rrule, confirm: true });
|
|
5001
5061
|
}
|
|
5002
5062
|
|
|
5003
5063
|
async function yandexCalendarList(args = {}) {
|
|
5004
5064
|
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5005
|
-
const baseUrl = await yandexCalendarBaseUrl(token);
|
|
5065
|
+
const baseUrl = await yandexCalendarBaseUrl(token, args);
|
|
5006
5066
|
const start = toIcsDate(args.start || new Date().toISOString());
|
|
5007
5067
|
const end = toIcsDate(args.end || new Date(Date.now() + 14 * 86400000).toISOString());
|
|
5008
5068
|
const body = `<?xml version="1.0"?>
|
|
@@ -5011,10 +5071,113 @@ async function yandexCalendarList(args = {}) {
|
|
|
5011
5071
|
<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
5072
|
</c:calendar-query>`;
|
|
5013
5073
|
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));
|
|
5074
|
+
return parseIcsEvents(text, { baseUrl }).slice(0, Number(args.limit || 20));
|
|
5075
|
+
}
|
|
5076
|
+
|
|
5077
|
+
async function yandexCalendarSearch(query, args = {}) {
|
|
5078
|
+
const needle = normalizeGeoText(query || args.query || args.title || "");
|
|
5079
|
+
const rows = await yandexCalendarList({
|
|
5080
|
+
...args,
|
|
5081
|
+
start: args.start || new Date(Date.now() - 90 * 86400000).toISOString(),
|
|
5082
|
+
end: args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
5083
|
+
limit: Math.max(200, Number(args.limit || 20) * 10),
|
|
5084
|
+
});
|
|
5085
|
+
if (!needle) return rows.slice(0, Number(args.limit || 20));
|
|
5086
|
+
return rows.filter((row) => normalizeGeoText([
|
|
5087
|
+
row.title,
|
|
5088
|
+
row.description,
|
|
5089
|
+
row.location,
|
|
5090
|
+
row.uid,
|
|
5091
|
+
].filter(Boolean).join(" ")).includes(needle)).slice(0, Number(args.limit || 20));
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
async function yandexCalendarGet(query, args = {}) {
|
|
5095
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5096
|
+
if (resolved.status !== "ok") return resolved;
|
|
5097
|
+
return stripCalendarPrivateFields(resolved.event);
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
async function yandexCalendarUpdate(query, args = {}) {
|
|
5101
|
+
if (!args.confirm) throw new Error("Для изменения события нужен аргумент confirm=true.");
|
|
5102
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5103
|
+
if (resolved.status !== "ok") return resolved;
|
|
5104
|
+
const event = resolved.event;
|
|
5105
|
+
const start = toIcsDate(args.start || args.date || event.startIso || event.start);
|
|
5106
|
+
const end = toIcsDate(args.end || event.endIso || event.end || new Date(new Date(args.start || event.startIso || Date.now()).getTime() + 3600000).toISOString());
|
|
5107
|
+
const next = buildIcsEvent({
|
|
5108
|
+
uid: event.uid || `${randomUUID()}@iola-cli`,
|
|
5109
|
+
start,
|
|
5110
|
+
end,
|
|
5111
|
+
summary: args.title || args.summary || event.title || "Событие IOLA",
|
|
5112
|
+
description: args.description ?? event.description ?? "",
|
|
5113
|
+
location: args.location ?? event.location ?? "",
|
|
5114
|
+
attendees: args.attendees || args.to || event.attendees || [],
|
|
5115
|
+
rrule: args.rrule === null ? "" : (args.rrule || event.rrule || buildIcsRrule(args)),
|
|
5116
|
+
reminders: args.reminders || args.reminder || args.alarm ? normalizeCalendarReminders(args.reminders || args.reminder || args.alarm) : event.reminders || [],
|
|
5117
|
+
});
|
|
5118
|
+
await yandexDavRequest(event.url, await requireYandexOAuthToken("organizer", "Яндекс Календарь"), { method: "PUT", ics: true, body: next, timeout: 45000 });
|
|
5119
|
+
return { status: "calendar-event-updated", uid: event.uid, title: args.title || event.title, href: event.href };
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5122
|
+
async function yandexCalendarMove(query, args = {}) {
|
|
5123
|
+
if (!args.confirm) throw new Error("Для переноса события нужен аргумент confirm=true.");
|
|
5124
|
+
const dateTime = args.start || args.date ? {} : extractDateTimeFromText(args.text || args.source_question || "");
|
|
5125
|
+
const start = args.start || args.date || dateTime.start;
|
|
5126
|
+
if (!start) throw new Error("Укажите новую дату/время события.");
|
|
5127
|
+
const end = args.end || dateTime.end || new Date(new Date(start).getTime() + 3600000).toISOString();
|
|
5128
|
+
return yandexCalendarUpdate(query, { ...args, start, end, confirm: true });
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
async function yandexCalendarAddReminder(query, args = {}) {
|
|
5132
|
+
if (!args.confirm) throw new Error("Для добавления напоминания нужен аргумент confirm=true.");
|
|
5133
|
+
const minutes = Number(args.minutes || args.beforeMinutes || String(args.reminder || "").match(/\d+/u)?.[0] || 15);
|
|
5134
|
+
return yandexCalendarUpdate(query, { ...args, reminders: [`-${minutes}`], confirm: true });
|
|
5015
5135
|
}
|
|
5016
5136
|
|
|
5017
|
-
async function
|
|
5137
|
+
async function yandexCalendarDelete(query, args = {}) {
|
|
5138
|
+
if (!args.confirm) throw new Error("Для удаления события нужен аргумент confirm=true.");
|
|
5139
|
+
const resolved = await resolveYandexCalendarEvent(query, args);
|
|
5140
|
+
if (resolved.status !== "ok") return resolved;
|
|
5141
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5142
|
+
await yandexDavRequest(resolved.event.url, token, { method: "DELETE", timeout: 45000 });
|
|
5143
|
+
return { status: "calendar-event-deleted", uid: resolved.event.uid, title: resolved.event.title, href: resolved.event.href };
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
async function resolveYandexCalendarEvent(query, args = {}) {
|
|
5147
|
+
const uid = String(args.uid || "").trim();
|
|
5148
|
+
const href = String(args.href || "").trim();
|
|
5149
|
+
if (href) {
|
|
5150
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
|
|
5151
|
+
const url = new URL(href, "https://caldav.yandex.ru/").toString();
|
|
5152
|
+
const ics = await yandexDavRequest(url, token, { method: "GET", timeout: 45000 });
|
|
5153
|
+
const event = parseIcsEvents(ics, { baseUrl: url.replace(/[^/]+$/u, "") })[0];
|
|
5154
|
+
return event ? { status: "ok", event: { ...event, href, url, ics } } : { status: "not-found", query: href };
|
|
5155
|
+
}
|
|
5156
|
+
const needle = normalizeGeoText(uid || query || args.query || args.title || "");
|
|
5157
|
+
const rows = await yandexCalendarSearch("", {
|
|
5158
|
+
...args,
|
|
5159
|
+
start: args.startRange || args.start || new Date(Date.now() - 365 * 86400000).toISOString(),
|
|
5160
|
+
end: args.endRange || args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
5161
|
+
limit: 500,
|
|
5162
|
+
});
|
|
5163
|
+
const matches = rows.filter((row) => {
|
|
5164
|
+
if (uid && row.uid === uid) return true;
|
|
5165
|
+
const haystack = normalizeGeoText([row.title, row.description, row.location, row.start, row.startIso, row.end, row.endIso, row.uid, row.href].filter(Boolean).join(" "));
|
|
5166
|
+
return needle && haystack.includes(needle);
|
|
5167
|
+
});
|
|
5168
|
+
if (!matches.length) return { status: "not-found", kind: "calendar-event", query: query || uid };
|
|
5169
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", kind: "calendar-event", query: query || uid, events: matches.slice(0, 10).map(stripCalendarPrivateFields) };
|
|
5170
|
+
return { status: "ok", event: matches[0] };
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
async function yandexCalendarBaseUrl(token, args = {}) {
|
|
5174
|
+
const calendars = await yandexCalendarCollections(token);
|
|
5175
|
+
const selected = pickYandexCalendar(calendars, args);
|
|
5176
|
+
if (!selected) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
|
|
5177
|
+
return selected.url;
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
async function yandexCalendarCollections(token) {
|
|
5018
5181
|
const root = "https://caldav.yandex.ru/";
|
|
5019
5182
|
const principalXml = await yandexDavRequest(root, token, {
|
|
5020
5183
|
method: "PROPFIND",
|
|
@@ -5038,9 +5201,99 @@ async function yandexCalendarBaseUrl(token) {
|
|
|
5038
5201
|
xml: true,
|
|
5039
5202
|
body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>",
|
|
5040
5203
|
});
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5204
|
+
return extractCalendarCollections(listXml).map((row) => ({ ...row, url: new URL(row.href, root).toString() }));
|
|
5205
|
+
}
|
|
5206
|
+
|
|
5207
|
+
function pickYandexCalendar(calendars, args = {}) {
|
|
5208
|
+
const query = normalizeGeoText(args.calendar || args.calendarName || args.name || "");
|
|
5209
|
+
if (query) {
|
|
5210
|
+
const found = calendars.find((row) => normalizeGeoText(`${row.name} ${row.href}`).includes(query));
|
|
5211
|
+
if (found) return found;
|
|
5212
|
+
}
|
|
5213
|
+
return calendars.find((row) => !/\/(?:inbox|outbox)\//iu.test(row.href)) || calendars[0] || null;
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
async function yandexDocsStatus() {
|
|
5217
|
+
const info = await yandexDiskInfo();
|
|
5218
|
+
await ensureYandexDiskDir(`${CLOUD_DEFAULT_REMOTE_DIR}/docs`, { allowExisting: true });
|
|
5219
|
+
return { status: "ok", provider: "yandex-disk", folder: `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, totalSpace: info.totalSpace, usedSpace: info.usedSpace };
|
|
5220
|
+
}
|
|
5221
|
+
|
|
5222
|
+
async function yandexDocsList(args = {}) {
|
|
5223
|
+
const folder = normalizeCloudUserPath(args.path || args.folder || `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, "yandex-disk");
|
|
5224
|
+
const rows = await yandexDiskListRecursive(folder, { depth: Number(args.depth || 3), limit: Number(args.limit || 100) }).catch(() => []);
|
|
5225
|
+
return rows.filter(isYandexDocumentResource).slice(0, Number(args.limit || 50));
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
async function yandexDocsFind(query, args = {}) {
|
|
5229
|
+
const needle = normalizeGeoText(query || args.query || "");
|
|
5230
|
+
const rows = await yandexDocsList({ ...args, limit: Math.max(200, Number(args.limit || 20) * 10) });
|
|
5231
|
+
if (!needle) return rows.slice(0, Number(args.limit || 20));
|
|
5232
|
+
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.path}`).includes(needle)).slice(0, Number(args.limit || 20));
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
async function yandexDocsCreateText(args = {}) {
|
|
5236
|
+
if (!args.confirm) throw new Error("Для создания документа нужен аргумент confirm=true.");
|
|
5237
|
+
const text = String(args.text || args.content || "").trim();
|
|
5238
|
+
if (!text) throw new Error("Текст документа пустой.");
|
|
5239
|
+
const ext = normalizeYandexDocExtension(args.format || args.ext || path.extname(args.path || args.remotePath || ""));
|
|
5240
|
+
const title = slugYandexDiskName(args.title || args.name || `document-${timestampForFile()}`);
|
|
5241
|
+
const remotePath = normalizeCloudUserPath(args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/docs/${title}${title.endsWith(ext) ? "" : ext}`, "yandex-disk");
|
|
5242
|
+
const saved = await yandexDiskSaveText(text, remotePath);
|
|
5243
|
+
return { ...saved, status: "document-created", title: path.posix.basename(remotePath), remote: remotePath };
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
async function yandexDocsRead(target, args = {}) {
|
|
5247
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5248
|
+
if (doc.status !== "ok") return doc;
|
|
5249
|
+
if (!/\.(txt|md|csv|json|html)$/iu.test(doc.path)) {
|
|
5250
|
+
return { status: "binary-document", remote: doc.path, name: doc.name, message: "Этот документ не текстовый. Его можно скачать, переименовать, удалить или опубликовать ссылкой." };
|
|
5251
|
+
}
|
|
5252
|
+
return yandexDiskReadText(doc.path, args);
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5255
|
+
async function yandexDocsShare(target, args = {}) {
|
|
5256
|
+
if (!args.confirm) throw new Error("Для публикации документа нужен аргумент confirm=true.");
|
|
5257
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5258
|
+
if (doc.status !== "ok") return doc;
|
|
5259
|
+
return yandexDiskShareWithQr(doc.path, { ...args, confirm: true });
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
async function yandexDocsRename(target, newName, args = {}) {
|
|
5263
|
+
if (!args.confirm) throw new Error("Для переименования документа нужен аргумент confirm=true.");
|
|
5264
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5265
|
+
if (doc.status !== "ok") return doc;
|
|
5266
|
+
if (!newName) throw new Error("Укажите новое имя документа.");
|
|
5267
|
+
return yandexDiskRename(doc.path, newName, { ...args, confirm: true, overwrite: args.overwrite !== false });
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
async function yandexDocsDelete(target, args = {}) {
|
|
5271
|
+
if (!args.confirm) throw new Error("Для удаления документа нужен аргумент confirm=true.");
|
|
5272
|
+
const doc = await resolveYandexDoc(target, args);
|
|
5273
|
+
if (doc.status !== "ok") return doc;
|
|
5274
|
+
return yandexDiskDelete(doc.path, args);
|
|
5275
|
+
}
|
|
5276
|
+
|
|
5277
|
+
async function resolveYandexDoc(target, args = {}) {
|
|
5278
|
+
const value = String(target || "").trim();
|
|
5279
|
+
if (value.startsWith("/")) {
|
|
5280
|
+
const statRow = await yandexDiskStat(value);
|
|
5281
|
+
return isYandexDocumentResource(statRow) ? { status: "ok", ...statRow } : { status: "not-document", path: value };
|
|
5282
|
+
}
|
|
5283
|
+
const rows = await yandexDocsFind(value, { ...args, limit: 10 });
|
|
5284
|
+
if (!rows.length) return { status: "not-found", query: value };
|
|
5285
|
+
if (rows.length > 1 && !args.selectFirst) return { status: "ambiguous", query: value, docs: rows.slice(0, 10) };
|
|
5286
|
+
return { status: "ok", ...rows[0] };
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
function isYandexDocumentResource(row = {}) {
|
|
5290
|
+
return row.type !== "dir" && /\.(docx|xlsx|pptx|pdf|txt|md|html|csv|json)$/iu.test(row.name || row.path || "");
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
function normalizeYandexDocExtension(value) {
|
|
5294
|
+
const ext = String(value || "").replace(/^\./u, "").toLocaleLowerCase("en-US");
|
|
5295
|
+
if (["txt", "md", "html", "csv", "json"].includes(ext)) return `.${ext}`;
|
|
5296
|
+
return ".md";
|
|
5044
5297
|
}
|
|
5045
5298
|
|
|
5046
5299
|
async function yandexContactsStatus() {
|
|
@@ -5639,25 +5892,92 @@ async function yandexContactsBaseUrl(token) {
|
|
|
5639
5892
|
return new URL(addressBookHref, root).toString();
|
|
5640
5893
|
}
|
|
5641
5894
|
|
|
5895
|
+
async function yandexTelemostStatus() {
|
|
5896
|
+
const calendar = await yandexCalendarStatus();
|
|
5897
|
+
return {
|
|
5898
|
+
status: "calendar-fallback",
|
|
5899
|
+
provider: "yandex-telemost",
|
|
5900
|
+
calendar: calendar.displayName,
|
|
5901
|
+
message: "Для обычного OAuth-подключения CLI использует календарное событие. Прямой Telemost API проверяется отдельно и может быть доступен только аккаунтам/организациям Яндекс 360.",
|
|
5902
|
+
};
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5642
5905
|
async function yandexTelemostCreateEvent(args = {}) {
|
|
5906
|
+
if (!args.confirm) throw new Error("Для создания встречи нужен аргумент confirm=true.");
|
|
5907
|
+
const telemost = await tryCreateYandexTelemostMeeting(args).catch((error) => ({
|
|
5908
|
+
status: "telemost-api-unavailable",
|
|
5909
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5910
|
+
}));
|
|
5643
5911
|
const description = [
|
|
5644
5912
|
args.description || "",
|
|
5645
5913
|
"",
|
|
5646
|
-
"Телемост:
|
|
5914
|
+
telemost.joinUrl ? `Ссылка Телемоста: ${telemost.joinUrl}` : "Телемост: прямое создание ссылки через API недоступно для текущего OAuth-подключения. Событие создано в календаре как встреча; ссылку можно добавить вручную в Яндекс Календаре.",
|
|
5647
5915
|
].join("\n").trim();
|
|
5648
|
-
|
|
5916
|
+
const event = await yandexCalendarCreateEvent({
|
|
5917
|
+
...args,
|
|
5918
|
+
title: args.title || args.summary || "Телемост IOLA",
|
|
5919
|
+
description,
|
|
5920
|
+
location: telemost.joinUrl || args.location || "Яндекс Телемост",
|
|
5921
|
+
confirm: true,
|
|
5922
|
+
});
|
|
5923
|
+
return { ...event, status: telemost.joinUrl ? "telemost-event-created" : "telemost-calendar-fallback-created", telemost };
|
|
5649
5924
|
}
|
|
5650
5925
|
|
|
5651
|
-
function
|
|
5926
|
+
async function tryCreateYandexTelemostMeeting(args = {}) {
|
|
5927
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Телемост");
|
|
5928
|
+
const endpoints = [
|
|
5929
|
+
"https://cloud-api.yandex.net/v1/telemost/meetings",
|
|
5930
|
+
"https://api360.yandex.net/v1/telemost/meetings",
|
|
5931
|
+
];
|
|
5932
|
+
let lastError = "";
|
|
5933
|
+
for (const endpoint of endpoints) {
|
|
5934
|
+
const response = await fetch(endpoint, {
|
|
5935
|
+
method: "POST",
|
|
5936
|
+
headers: {
|
|
5937
|
+
Authorization: `OAuth ${token}`,
|
|
5938
|
+
"content-type": "application/json",
|
|
5939
|
+
},
|
|
5940
|
+
body: JSON.stringify({
|
|
5941
|
+
title: args.title || args.summary || "Телемост IOLA",
|
|
5942
|
+
description: args.description || "",
|
|
5943
|
+
}),
|
|
5944
|
+
signal: AbortSignal.timeout(15000),
|
|
5945
|
+
}).catch((error) => ({ ok: false, status: 0, statusText: error.message, text: async () => error.message }));
|
|
5946
|
+
const text = await response.text().catch(() => "");
|
|
5947
|
+
if (response.ok) {
|
|
5948
|
+
const payload = text ? JSON.parse(text) : {};
|
|
5949
|
+
return {
|
|
5950
|
+
status: "telemost-api-created",
|
|
5951
|
+
endpoint,
|
|
5952
|
+
id: payload.id || payload.meeting_id || "",
|
|
5953
|
+
joinUrl: payload.join_url || payload.url || payload.link || payload.meeting_url || "",
|
|
5954
|
+
raw: payload,
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
lastError = `${endpoint}: ${response.status} ${response.statusText} ${sanitizeSecretFromText(text.slice(0, 500), token)}`;
|
|
5958
|
+
if (![404, 405].includes(Number(response.status))) break;
|
|
5959
|
+
}
|
|
5960
|
+
throw new Error(lastError || "Telemost API недоступен.");
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5963
|
+
function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [], rrule = "", reminders = [] }) {
|
|
5652
5964
|
const escape = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
5653
5965
|
const attendeeLines = (Array.isArray(attendees) ? attendees : [attendees])
|
|
5654
5966
|
.map((email) => String(email || "").trim())
|
|
5655
5967
|
.filter((email) => /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email))
|
|
5656
5968
|
.map((email) => `ATTENDEE;CN=${escape(email)};ROLE=REQ-PARTICIPANT:mailto:${email}`);
|
|
5969
|
+
const reminderBlocks = normalizeCalendarReminders(reminders).map((minutes) => [
|
|
5970
|
+
"BEGIN:VALARM",
|
|
5971
|
+
`TRIGGER:-PT${Math.max(1, Math.abs(Number(minutes) || 15))}M`,
|
|
5972
|
+
"ACTION:DISPLAY",
|
|
5973
|
+
`DESCRIPTION:${escape(summary || "Напоминание")}`,
|
|
5974
|
+
"END:VALARM",
|
|
5975
|
+
].join("\r\n"));
|
|
5657
5976
|
return [
|
|
5658
5977
|
"BEGIN:VCALENDAR",
|
|
5659
5978
|
"VERSION:2.0",
|
|
5660
5979
|
"PRODID:-//IOLA CLI//Yandex Calendar//RU",
|
|
5980
|
+
"CALSCALE:GREGORIAN",
|
|
5661
5981
|
"BEGIN:VEVENT",
|
|
5662
5982
|
`UID:${uid}`,
|
|
5663
5983
|
`DTSTAMP:${toIcsDate(new Date().toISOString())}`,
|
|
@@ -5666,7 +5986,9 @@ function buildIcsEvent({ uid, start, end, summary, description, location, attend
|
|
|
5666
5986
|
`SUMMARY:${escape(summary)}`,
|
|
5667
5987
|
description ? `DESCRIPTION:${escape(description)}` : "",
|
|
5668
5988
|
location ? `LOCATION:${escape(location)}` : "",
|
|
5989
|
+
rrule ? `RRULE:${rrule}` : "",
|
|
5669
5990
|
...attendeeLines,
|
|
5991
|
+
...reminderBlocks,
|
|
5670
5992
|
"END:VEVENT",
|
|
5671
5993
|
"END:VCALENDAR",
|
|
5672
5994
|
"",
|
|
@@ -5679,15 +6001,105 @@ function toIcsDate(value) {
|
|
|
5679
6001
|
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/u, "Z");
|
|
5680
6002
|
}
|
|
5681
6003
|
|
|
5682
|
-
function parseIcsEvents(xmlOrIcs) {
|
|
5683
|
-
const
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
6004
|
+
function parseIcsEvents(xmlOrIcs, options = {}) {
|
|
6005
|
+
const text = String(xmlOrIcs || "");
|
|
6006
|
+
const responses = text.split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
6007
|
+
if (responses.length) {
|
|
6008
|
+
return responses.flatMap((response) => {
|
|
6009
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
6010
|
+
const calendarData = response.match(/<[^:>]*:?calendar-data[^>]*>([\s\S]*?)<\/[^:>]*:?calendar-data>/iu)?.[1] || response;
|
|
6011
|
+
return parseIcsEvents(calendarData, options).map((row) => ({
|
|
6012
|
+
...row,
|
|
6013
|
+
href: row.href || href,
|
|
6014
|
+
url: row.url || (href ? new URL(href, options.baseUrl || "https://caldav.yandex.ru/").toString() : ""),
|
|
6015
|
+
}));
|
|
6016
|
+
});
|
|
6017
|
+
}
|
|
6018
|
+
const decoded = decodeXml(stripXmlTags(text));
|
|
6019
|
+
return decoded.split("BEGIN:VEVENT").slice(1).map((chunk) => {
|
|
6020
|
+
const lines = unfoldIcsLines(`BEGIN:VEVENT${chunk}`);
|
|
6021
|
+
const uid = icsValue(lines, "UID");
|
|
6022
|
+
const start = icsValue(lines, "DTSTART");
|
|
6023
|
+
const end = icsValue(lines, "DTEND");
|
|
6024
|
+
return {
|
|
6025
|
+
uid,
|
|
6026
|
+
title: unescapeIcsValue(icsValue(lines, "SUMMARY")),
|
|
6027
|
+
start,
|
|
6028
|
+
end,
|
|
6029
|
+
startIso: icsDateToIso(start),
|
|
6030
|
+
endIso: icsDateToIso(end),
|
|
6031
|
+
location: unescapeIcsValue(icsValue(lines, "LOCATION")),
|
|
6032
|
+
description: unescapeIcsValue(icsValue(lines, "DESCRIPTION")),
|
|
6033
|
+
rrule: icsValue(lines, "RRULE"),
|
|
6034
|
+
attendees: icsValues(lines, "ATTENDEE").map((value) => value.replace(/^mailto:/iu, "")),
|
|
6035
|
+
reminders: parseIcsReminderMinutes(lines),
|
|
6036
|
+
ics: decoded.includes("BEGIN:VCALENDAR") ? decoded : "",
|
|
6037
|
+
};
|
|
6038
|
+
}).filter((item) => item.uid || item.title);
|
|
6039
|
+
}
|
|
6040
|
+
|
|
6041
|
+
function unfoldIcsLines(value) {
|
|
6042
|
+
return String(value || "")
|
|
6043
|
+
.replace(/\r/g, "")
|
|
6044
|
+
.replace(/\n[ \t]/g, "")
|
|
6045
|
+
.split(/\n/u)
|
|
6046
|
+
.map((line) => line.trim())
|
|
6047
|
+
.filter(Boolean);
|
|
6048
|
+
}
|
|
6049
|
+
|
|
6050
|
+
function icsValues(lines, property) {
|
|
6051
|
+
const prop = String(property || "").toLocaleUpperCase("en-US");
|
|
6052
|
+
return lines
|
|
6053
|
+
.filter((line) => line.toLocaleUpperCase("en-US").startsWith(`${prop};`) || line.toLocaleUpperCase("en-US").startsWith(`${prop}:`))
|
|
6054
|
+
.map((line) => line.slice(line.indexOf(":") + 1).trim())
|
|
6055
|
+
.filter(Boolean);
|
|
6056
|
+
}
|
|
6057
|
+
|
|
6058
|
+
function icsValue(lines, property) {
|
|
6059
|
+
return icsValues(lines, property)[0] || "";
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
function unescapeIcsValue(value) {
|
|
6063
|
+
return String(value || "")
|
|
6064
|
+
.replace(/\\n/giu, "\n")
|
|
6065
|
+
.replace(/\\,/gu, ",")
|
|
6066
|
+
.replace(/\\;/gu, ";")
|
|
6067
|
+
.replace(/\\\\/gu, "\\")
|
|
6068
|
+
.trim();
|
|
6069
|
+
}
|
|
6070
|
+
|
|
6071
|
+
function icsDateToIso(value) {
|
|
6072
|
+
const text = String(value || "").trim();
|
|
6073
|
+
const match = text.match(/^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/u);
|
|
6074
|
+
if (!match) return "";
|
|
6075
|
+
const iso = `${match[1]}-${match[2]}-${match[3]}T${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}${text.endsWith("Z") ? "Z" : ""}`;
|
|
6076
|
+
const date = new Date(iso);
|
|
6077
|
+
return Number.isNaN(date.getTime()) ? "" : date.toISOString();
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6080
|
+
function parseIcsReminderMinutes(lines) {
|
|
6081
|
+
return lines
|
|
6082
|
+
.filter((line) => /^TRIGGER:/iu.test(line))
|
|
6083
|
+
.map((line) => Number(line.match(/PT(\d+)M/iu)?.[1] || 0))
|
|
6084
|
+
.filter(Boolean);
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
function normalizeCalendarReminders(value) {
|
|
6088
|
+
const rows = Array.isArray(value) ? value : String(value || "").split(/[;,]/u);
|
|
6089
|
+
return rows.map((item) => Number(String(item).match(/\d+/u)?.[0] || 0)).filter(Boolean);
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
function buildIcsRrule(args = {}) {
|
|
6093
|
+
const repeat = String(args.repeat || args.frequency || "").toLocaleLowerCase("ru-RU");
|
|
6094
|
+
if (!repeat && !args.count && !args.until) return "";
|
|
6095
|
+
const freq = /день|daily|day/iu.test(repeat) ? "DAILY"
|
|
6096
|
+
: /месяц|monthly|month/iu.test(repeat) ? "MONTHLY"
|
|
6097
|
+
: /год|year|yearly|annual/iu.test(repeat) ? "YEARLY"
|
|
6098
|
+
: "WEEKLY";
|
|
6099
|
+
const parts = [`FREQ=${freq}`];
|
|
6100
|
+
if (args.count) parts.push(`COUNT=${Number(args.count)}`);
|
|
6101
|
+
if (args.until) parts.push(`UNTIL=${toIcsDate(args.until)}`);
|
|
6102
|
+
return parts.join(";");
|
|
5691
6103
|
}
|
|
5692
6104
|
|
|
5693
6105
|
function extractDavHref(xml, propertyName) {
|
|
@@ -5706,6 +6118,24 @@ function extractCalendarCollectionHref(xml) {
|
|
|
5706
6118
|
return "";
|
|
5707
6119
|
}
|
|
5708
6120
|
|
|
6121
|
+
function extractCalendarCollections(xml) {
|
|
6122
|
+
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
6123
|
+
const rows = [];
|
|
6124
|
+
for (const response of responses) {
|
|
6125
|
+
if (!/<[^:>]*:?calendar(?:\s|>|\/)/iu.test(response)) continue;
|
|
6126
|
+
const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
|
|
6127
|
+
if (!href || /\/(?:inbox|outbox)\//iu.test(href)) continue;
|
|
6128
|
+
const name = stripXmlTags(response.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "").trim() || path.posix.basename(href.replace(/\/$/u, ""));
|
|
6129
|
+
rows.push({ provider: "yandex-calendar", href, name, type: "calendar" });
|
|
6130
|
+
}
|
|
6131
|
+
return rows;
|
|
6132
|
+
}
|
|
6133
|
+
|
|
6134
|
+
function stripCalendarPrivateFields(row = {}) {
|
|
6135
|
+
const { ics: _ics, url: _url, ...rest } = row;
|
|
6136
|
+
return rest;
|
|
6137
|
+
}
|
|
6138
|
+
|
|
5709
6139
|
function extractAddressBookCollectionHref(xml) {
|
|
5710
6140
|
const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
|
|
5711
6141
|
for (const response of responses) {
|
|
@@ -11356,10 +11786,93 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
11356
11786
|
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
11357
11787
|
}
|
|
11358
11788
|
|
|
11359
|
-
if (/(
|
|
11789
|
+
if (/(документ|документы|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
|
|
11790
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
11791
|
+
const result = await yandexDocsStatus();
|
|
11792
|
+
return `Яндекс Документы через Диск подключены. Папка: ${result.folder}.`;
|
|
11793
|
+
}
|
|
11794
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
|
|
11795
|
+
const text = extractShareMessage(question) || cleanupCloudSaveText(question);
|
|
11796
|
+
if (!text) return "Укажите текст документа. Пример: создай документ на Яндекс Диске текст: ...";
|
|
11797
|
+
const result = await yandexDocsCreateText({
|
|
11798
|
+
title: extractYandexDocTitle(question),
|
|
11799
|
+
text,
|
|
11800
|
+
format: /html/iu.test(normalized) ? "html" : /txt|текстов/iu.test(normalized) ? "txt" : "md",
|
|
11801
|
+
confirm: true,
|
|
11802
|
+
});
|
|
11803
|
+
return `Документ создан на Яндекс Диске: ${result.remote}.`;
|
|
11804
|
+
}
|
|
11805
|
+
if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
|
|
11806
|
+
const result = await yandexDocsRead(extractCloudPath(question) || cleanupYandexQuery(question), {});
|
|
11807
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11808
|
+
}
|
|
11809
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
|
|
11810
|
+
const result = await yandexDocsShare(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
|
|
11811
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11812
|
+
}
|
|
11813
|
+
if (/(переимен|rename)/iu.test(normalized)) {
|
|
11814
|
+
const result = await yandexDocsRename(extractCloudPath(question) || cleanupYandexQuery(question), extractCloudNewName(question), { confirm: true });
|
|
11815
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11816
|
+
}
|
|
11817
|
+
if (/(удали|удалить)/iu.test(normalized)) {
|
|
11818
|
+
const result = await yandexDocsDelete(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
|
|
11819
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11820
|
+
}
|
|
11821
|
+
const rows = /(найди|поиск)/iu.test(normalized)
|
|
11822
|
+
? await yandexDocsFind(cleanupYandexQuery(question), { limit: 20 })
|
|
11823
|
+
: await yandexDocsList({ limit: 20 });
|
|
11824
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
11825
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
11826
|
+
}
|
|
11827
|
+
|
|
11828
|
+
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
11829
|
+
if (/(статус|проверь|работает|доступ)/iu.test(normalized) && /(календар|телемост)/iu.test(normalized)) {
|
|
11830
|
+
const result = /телемост/iu.test(normalized) ? await yandexTelemostStatus() : await yandexCalendarStatus();
|
|
11831
|
+
return /телемост/iu.test(normalized)
|
|
11832
|
+
? `${result.message} Календарь: ${result.calendar || "-"}.`
|
|
11833
|
+
: `Яндекс Календарь подключен: ${result.displayName || result.url}. Календарей: ${result.calendars || 1}.`;
|
|
11834
|
+
}
|
|
11835
|
+
if (/(какие|список).{0,30}(календар|календари)|календари/iu.test(normalized)) {
|
|
11836
|
+
const rows = await yandexCalendarCalendars({ limit: 20 });
|
|
11837
|
+
if (!rows.length) return "Календари Яндекса не найдены.";
|
|
11838
|
+
return ["Яндекс Календари:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.href}`)].join("\n");
|
|
11839
|
+
}
|
|
11840
|
+
if (/(напомин|уведом)/iu.test(normalized) && /(добав|постав|создай)/iu.test(normalized)) {
|
|
11841
|
+
const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
|
|
11842
|
+
const result = await yandexCalendarAddReminder(cleanupCalendarEventQuery(question), { minutes, confirm: true });
|
|
11843
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11844
|
+
}
|
|
11845
|
+
if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
|
|
11846
|
+
const dateTime = extractDateTimeFromText(question);
|
|
11847
|
+
const title = extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA");
|
|
11848
|
+
const args = { ...dateTime, title, description: extractShareMessage(question) || "", confirm: true };
|
|
11849
|
+
const result = /телемост/iu.test(normalized)
|
|
11850
|
+
? await yandexTelemostCreateEvent(args)
|
|
11851
|
+
: /кажд|еженед|ежеднев|ежемесяч|повтор/iu.test(normalized)
|
|
11852
|
+
? await yandexCalendarCreateRecurringEvent({ ...args, ...extractCalendarRepeat(question), confirm: true })
|
|
11853
|
+
: await yandexCalendarCreateEvent(args);
|
|
11854
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11855
|
+
}
|
|
11856
|
+
if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) {
|
|
11857
|
+
const result = await yandexCalendarMove(cleanupCalendarEventQuery(question), { ...extractDateTimeFromText(question), confirm: true });
|
|
11858
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11859
|
+
}
|
|
11860
|
+
if (/(переимен|измени\s+назв|смени\s+назв)/iu.test(normalized)) {
|
|
11861
|
+
const result = await yandexCalendarUpdate(cleanupCalendarEventQuery(question), { title: extractCalendarTitle(question), confirm: true });
|
|
11862
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11863
|
+
}
|
|
11864
|
+
if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) {
|
|
11865
|
+
const result = await yandexCalendarDelete(cleanupCalendarEventQuery(question), { confirm: true });
|
|
11866
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
11867
|
+
}
|
|
11868
|
+
if (/(найди|поиск|где|покажи).{0,40}(событи|встреч|телемост)/iu.test(normalized)) {
|
|
11869
|
+
const rows = await yandexCalendarSearch(cleanupCalendarEventQuery(question), { limit: 20 });
|
|
11870
|
+
if (!rows.length) return "События в Яндекс Календаре по запросу не найдены.";
|
|
11871
|
+
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
|
|
11872
|
+
}
|
|
11360
11873
|
const rows = await yandexCalendarList({ limit: 10 });
|
|
11361
11874
|
if (!rows.length) return "В ближайшие дни событий в Яндекс Календаре не найдено.";
|
|
11362
|
-
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.start || "-"}`)].join("\n");
|
|
11875
|
+
return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
|
|
11363
11876
|
}
|
|
11364
11877
|
|
|
11365
11878
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
@@ -11682,7 +12195,7 @@ async function buildUserSkillDirectAnswer(question) {
|
|
|
11682
12195
|
}
|
|
11683
12196
|
|
|
11684
12197
|
function isYandexServiceQuestion(normalized) {
|
|
11685
|
-
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex
|
|
12198
|
+
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
|
|
11686
12199
|
}
|
|
11687
12200
|
|
|
11688
12201
|
function isYandexIdentityQuestion(normalized) {
|
|
@@ -11967,6 +12480,38 @@ async function buildCloudDirectAnswer(question) {
|
|
|
11967
12480
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
11968
12481
|
try {
|
|
11969
12482
|
const provider = await getCloudProvider();
|
|
12483
|
+
if (provider === "yandex-disk" && /(документ|docs|360)/iu.test(normalized)) {
|
|
12484
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
|
|
12485
|
+
const result = await yandexDocsShare(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
|
|
12486
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12487
|
+
}
|
|
12488
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
|
|
12489
|
+
const text = extractShareMessage(question) || cleanupCloudSaveText(question);
|
|
12490
|
+
if (!text) return "Укажите текст документа.";
|
|
12491
|
+
const result = await yandexDocsCreateText({ title: extractYandexDocTitle(question), text, confirm: true });
|
|
12492
|
+
return `Документ создан на Яндекс Диске: ${result.remote}.`;
|
|
12493
|
+
}
|
|
12494
|
+
if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
|
|
12495
|
+
const result = await yandexDocsRead(extractCloudPath(question) || cleanupCloudQuery(question), {});
|
|
12496
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12497
|
+
}
|
|
12498
|
+
if (/(переимен|rename)/iu.test(normalized)) {
|
|
12499
|
+
const result = await yandexDocsRename(extractCloudPath(question) || cleanupCloudQuery(question), extractCloudNewName(question), { confirm: true });
|
|
12500
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12501
|
+
}
|
|
12502
|
+
if (/(удали|удалить)/iu.test(normalized)) {
|
|
12503
|
+
const result = await yandexDocsDelete(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
|
|
12504
|
+
return formatToolResult({ rows: [result], outputs: [] }, {});
|
|
12505
|
+
}
|
|
12506
|
+
if (/(найди|поиск)/iu.test(normalized)) {
|
|
12507
|
+
const rows = await yandexDocsFind(cleanupCloudQuery(question), { limit: 20 });
|
|
12508
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
12509
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
12510
|
+
}
|
|
12511
|
+
const rows = await yandexDocsList({ limit: 20 });
|
|
12512
|
+
if (!rows.length) return "Документы на Яндекс Диске не найдены.";
|
|
12513
|
+
return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
|
|
12514
|
+
}
|
|
11970
12515
|
if (provider === "yandex-disk" && /(мест[оа]|сколько.*занято|сколько.*свобод|инфо|статус)/iu.test(normalized)) {
|
|
11971
12516
|
const info = await yandexDiskInfo();
|
|
11972
12517
|
return [
|
|
@@ -12200,6 +12745,7 @@ function extractCloudPath(question) {
|
|
|
12200
12745
|
function cleanupCloudPathCandidate(value) {
|
|
12201
12746
|
return String(value || "")
|
|
12202
12747
|
.replace(/\s+(?:по\s+почт[еуы]|на\s+почт[уые]|контакту|получател[юя]|кому|с\s+темой|тема\s*:|текст\s*:).*$/iu, "")
|
|
12748
|
+
.replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, "")
|
|
12203
12749
|
.replace(/[.!?]+$/u, "")
|
|
12204
12750
|
.trim();
|
|
12205
12751
|
}
|
|
@@ -12249,7 +12795,50 @@ function extractMailSubject(question) {
|
|
|
12249
12795
|
}
|
|
12250
12796
|
|
|
12251
12797
|
function extractShareMessage(question) {
|
|
12252
|
-
return String(question || "").match(/(
|
|
12798
|
+
return String(question || "").match(/(?:текст|text|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
|
|
12799
|
+
}
|
|
12800
|
+
|
|
12801
|
+
function extractYandexDocTitle(question) {
|
|
12802
|
+
const text = String(question || "");
|
|
12803
|
+
const raw = text.match(/(?:названи(?:е|ем)|имя|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12804
|
+
|| text.match(/(?:документ|файл)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12805
|
+
|| "";
|
|
12806
|
+
return raw.replace(/\s+(?:текст|text|body|сообщение)\s*:?.*$/iu, "").trim();
|
|
12807
|
+
}
|
|
12808
|
+
|
|
12809
|
+
function extractCalendarTitle(question) {
|
|
12810
|
+
const text = String(question || "");
|
|
12811
|
+
const raw = text.match(/(?:тема|названи(?:е|ем)|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12812
|
+
|| text.match(/(?:событи[ея]|встреч[ауи]|телемост)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
|
|
12813
|
+
|| "";
|
|
12814
|
+
return raw
|
|
12815
|
+
.replace(/\s+(?:сегодня|завтра|послезавтра)(?:\s|$).*$/iu, "")
|
|
12816
|
+
.replace(/\s+(?:в\s+\d{1,2}(?::\d{2})?|на\s+\d{1,2}[.\-/]\d{1,2}[\d.\-/]*)(?:\s|$).*$/iu, "")
|
|
12817
|
+
.trim();
|
|
12818
|
+
}
|
|
12819
|
+
|
|
12820
|
+
function extractCalendarRepeat(question) {
|
|
12821
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
12822
|
+
const count = Number(text.match(/(\d+)\s*(?:раз|повтор)/iu)?.[1] || 0);
|
|
12823
|
+
return {
|
|
12824
|
+
repeat: /ежеднев|каждый\s+день/iu.test(text) ? "daily"
|
|
12825
|
+
: /ежемесяч|каждый\s+месяц/iu.test(text) ? "monthly"
|
|
12826
|
+
: /ежегод|каждый\s+год/iu.test(text) ? "yearly"
|
|
12827
|
+
: "weekly",
|
|
12828
|
+
count: count || undefined,
|
|
12829
|
+
};
|
|
12830
|
+
}
|
|
12831
|
+
|
|
12832
|
+
function cleanupCalendarEventQuery(question) {
|
|
12833
|
+
return String(question || "")
|
|
12834
|
+
.replace(/(?:создай|добавь|запланируй|назначь|перенеси|перемести|измени|смени|удали|удалить|отмени|отменить|найди|поиск|покажи|добавь\s+напоминание|поставь\s+напоминание)/giu, " ")
|
|
12835
|
+
.replace(/(?:^|\s)(?:событи\p{L}*|встреч\p{L}*|телемост\p{L}*|календар\p{L}*|напомин\p{L}*|уведом\p{L}*|яндекс|на|к|ко|в|во|сегодня|завтра|послезавтра|час|часа|часов|минут|минуты)(?=\s|$)/giu, " ")
|
|
12836
|
+
.replace(/\d{1,2}[.\-/]\d{1,2}(?:[.\-/]\d{2,4})?/gu, " ")
|
|
12837
|
+
.replace(/\d{1,2}[:.]\d{2}/gu, " ")
|
|
12838
|
+
.replace(/(?:^|\s)\d{1,3}(?=\s|$)/gu, " ")
|
|
12839
|
+
.replace(/[,:;.!?«»"()]+/gu, " ")
|
|
12840
|
+
.replace(/\s+/g, " ")
|
|
12841
|
+
.trim();
|
|
12253
12842
|
}
|
|
12254
12843
|
|
|
12255
12844
|
function parseYandexDiskPackageRequest(question) {
|
|
@@ -12826,8 +13415,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
12826
13415
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
12827
13416
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
12828
13417
|
"Минимальные 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.",
|
|
13418
|
+
"Yandex tools: yandex_identity_me {}, yandex_disk_info {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_stat {path}, yandex_disk_exists {path}, yandex_disk_read_text {path}, yandex_disk_save_text {path,text}, yandex_disk_upload {localPath,remotePath}, yandex_disk_download {remotePath,outputPath}, yandex_disk_move {from,to,confirm}, yandex_disk_copy {from,to,confirm}, yandex_disk_rename {path,name,confirm}, yandex_disk_share {path,confirm}, yandex_disk_share_qr {path,confirm}, yandex_disk_share_email {path,to,contact,subject,text,confirm}, yandex_disk_package_share_email {sourcePath,targetFolder,to,contact,mode,confirm}, yandex_disk_unshare {path}, yandex_disk_delete {path,confirm}, yandex_disk_trash_list {}, yandex_disk_restore {path,confirm}, yandex_disk_empty_trash {confirm}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_send {to,subject,text,confirm}, yandex_mail_reply {uid,text,confirm}, yandex_mail_forward {uid,to,confirm}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_calendar_calendars {}, yandex_calendar_list {start,end}, yandex_calendar_search {query,start,end}, yandex_calendar_get {query}, yandex_calendar_create_event {title,start,end,location,attendees,reminders,confirm}, yandex_calendar_update {query,title,start,end,location,description,reminders,confirm}, yandex_calendar_move {query,start,end,confirm}, yandex_calendar_delete {query,confirm}, yandex_docs_list {path}, yandex_docs_find {query}, yandex_docs_create_text {title,text,format,confirm}, yandex_docs_read {path|query}, yandex_docs_share {path|query,confirm}, yandex_docs_rename {path|query,name,confirm}, yandex_docs_delete {path|query,confirm}, yandex_contacts_list {limit}, yandex_contacts_search {query}, yandex_contacts_get {query}, yandex_contacts_create {name,email,phone,address,note,confirm}, yandex_contacts_update {query,email,phone,address,note,birthday,org,title,confirm}, yandex_contacts_delete {query,confirm}, yandex_contacts_export_csv {}, yandex_contacts_find_incomplete {}, yandex_contacts_find_duplicates {}, yandex_contacts_backup_to_disk {format,confirm}, yandex_contact_send_mail {contact,subject,text,confirm}, yandex_contact_send_disk_link_qr {contact,path,confirm}, yandex_contact_create_disk_folder {contact,confirm}, yandex_contact_create_calendar_event {contact,start,end,title,confirm}, yandex_contact_create_telemost_event {contact,start,end,title,confirm}.",
|
|
13419
|
+
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_calendar_create_event, yandex_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event.",
|
|
12831
13420
|
"User skill tools: user_skill_create {name,description,instructions,tools,enable,confirm}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай skill только по явной просьбе пользователя и с confirm=true.",
|
|
12832
13421
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
12833
13422
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
@@ -12942,8 +13531,25 @@ function inferToolPlan(question, options = {}) {
|
|
|
12942
13531
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
|
|
12943
13532
|
return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
|
|
12944
13533
|
}
|
|
13534
|
+
if (/(документ|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
|
|
13535
|
+
const target = extractCloudPath(question) || cleanupYandexQuery(question);
|
|
13536
|
+
if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_create_text", args: { title: extractYandexDocTitle(question), text: extractShareMessage(question) || cleanupCloudSaveText(question), confirm: true } }] };
|
|
13537
|
+
if (/(прочитай|открой|текст)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_read", args: { query: target } }] };
|
|
13538
|
+
if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_share", args: { query: target, confirm: true } }] };
|
|
13539
|
+
if (/(переимен|rename)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_rename", args: { query: target, name: extractCloudNewName(question), confirm: true } }] };
|
|
13540
|
+
if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_delete", args: { query: target, confirm: true } }] };
|
|
13541
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_find", args: { query: cleanupYandexQuery(question), limit: 20 } }] };
|
|
13542
|
+
return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
|
|
13543
|
+
}
|
|
12945
13544
|
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
12946
|
-
|
|
13545
|
+
if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
|
|
13546
|
+
const dateTime = extractDateTimeFromText(question);
|
|
13547
|
+
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 } }] };
|
|
13548
|
+
}
|
|
13549
|
+
if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_move", args: { query: cleanupCalendarEventQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
|
|
13550
|
+
if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_delete", args: { query: cleanupCalendarEventQuery(question), confirm: true } }] };
|
|
13551
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_search", args: { query: cleanupCalendarEventQuery(question), limit: 20 } }] };
|
|
13552
|
+
return { steps: [{ tool: "yandex_calendar_list", args: { limit: 20 } }] };
|
|
12947
13553
|
}
|
|
12948
13554
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
12949
13555
|
if (/(дубликат|повтор)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_duplicates", args: { limit: 20 } }] };
|
|
@@ -13537,6 +14143,18 @@ function formatToolResult(result, options) {
|
|
|
13537
14143
|
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
13538
14144
|
}
|
|
13539
14145
|
if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
|
|
14146
|
+
if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
|
|
14147
|
+
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 недоступна, создано событие календаря." : ""}`;
|
|
14148
|
+
}
|
|
14149
|
+
if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
|
|
14150
|
+
if (row.status === "calendar-event-deleted") return `Событие удалено: ${row.title || row.uid}`;
|
|
14151
|
+
if (row.status === "ambiguous" && row.events) return [`Нашел несколько событий. Уточните:`, ...row.events.map((event, index) => `${index + 1}. ${event.title || event.uid} — ${event.startIso || event.start || "-"}`)].join("\n");
|
|
14152
|
+
if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
|
|
14153
|
+
if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
|
|
14154
|
+
if (row.status === "binary-document") return `${row.name || row.remote}: ${row.message}`;
|
|
14155
|
+
if (row.status === "not-document") return `Это не документ: ${row.path}`;
|
|
14156
|
+
if (row.status === "ambiguous" && row.docs) return [`Нашел несколько документов. Уточните:`, ...row.docs.map((doc, index) => `${index + 1}. ${doc.name} — ${doc.path}`)].join("\n");
|
|
14157
|
+
if (row.status === "not-found" && row.docs !== undefined) return `Документ не найден: ${row.query}`;
|
|
13540
14158
|
if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
|
|
13541
14159
|
if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
|
|
13542
14160
|
if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
|
|
@@ -15554,7 +16172,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
15554
16172
|
if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
|
|
15555
16173
|
const cloudQuestion = isCloudQuestion(normalized);
|
|
15556
16174
|
if (enabled.has("personal-docs") && cloudQuestion) selected.add("personal-docs");
|
|
15557
|
-
if (enabled.has("yandex-services") && /(
|
|
16175
|
+
if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|документ|docs|360|облако)/iu.test(normalized)) selected.add("yandex-services");
|
|
15558
16176
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
15559
16177
|
if (enabled.has("local-files") && !cloudQuestion && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
15560
16178
|
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
@@ -68,8 +68,9 @@ Toolset `yandex` включает локальные tools для пользов
|
|
|
68
68
|
- Yandex ID: профиль, логин, email;
|
|
69
69
|
- Яндекс Диск: список, создание папок, поиск, сохранение текста, загрузка, скачивание, публичные ссылки;
|
|
70
70
|
- Яндекс Почта: статус, список писем, поиск, чтение, отправка;
|
|
71
|
-
- Яндекс Календарь:
|
|
71
|
+
- Яндекс Календарь: календари, список, поиск, создание, перенос, редактирование, повторы, напоминания, удаление событий;
|
|
72
|
+
- Яндекс Документы / 360: работа через Яндекс Диск - список, поиск, создание текстовых документов, чтение, ссылка/QR, переименование, удаление;
|
|
72
73
|
- Яндекс Контакты: статус, список, поиск, карточка, создание, обновление, удаление, импорт/экспорт, дубликаты, неполные карточки, backup на Диск, дни рождения в календарь, письмо/ссылка/встреча по контакту;
|
|
73
|
-
- Телемост:
|
|
74
|
+
- Телемост: попытка прямого API и fallback через календарное событие, если API недоступен аккаунту.
|
|
74
75
|
|
|
75
|
-
Опасные действия ограничены: отправка письма, удаление файлов, публикация
|
|
76
|
+
Опасные действия ограничены: отправка письма, удаление файлов, публикация ссылок, создание/изменение документов и создание/изменение/удаление событий требуют явного подтверждения в tool-вызове. Токены хранятся локально в `~/.iola/secrets.json`.
|
package/wiki/Yandex-Connector.md
CHANGED
|
@@ -143,8 +143,24 @@ QR-код:
|
|
|
143
143
|
- `yandex_mail_read` - прочитать письмо по UID;
|
|
144
144
|
- `yandex_mail_send` - отправить письмо;
|
|
145
145
|
- `yandex_calendar_status` - проверить доступ к Календарю;
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
146
|
+
- `yandex_calendar_calendars` - показать доступные календари;
|
|
147
|
+
- `yandex_calendar_list` - показать ближайшие события;
|
|
148
|
+
- `yandex_calendar_search` - найти событие;
|
|
149
|
+
- `yandex_calendar_get` - открыть карточку события;
|
|
150
|
+
- `yandex_calendar_create_event` - создать событие с участниками, местом и напоминаниями;
|
|
151
|
+
- `yandex_calendar_create_recurring_event` - создать повторяющееся событие;
|
|
152
|
+
- `yandex_calendar_update` - изменить событие;
|
|
153
|
+
- `yandex_calendar_move` - перенести событие;
|
|
154
|
+
- `yandex_calendar_add_reminder` - добавить напоминание;
|
|
155
|
+
- `yandex_calendar_delete` - удалить событие;
|
|
156
|
+
- `yandex_docs_status` - проверить Документы / 360 через Диск;
|
|
157
|
+
- `yandex_docs_list` - показать документы;
|
|
158
|
+
- `yandex_docs_find` - найти документ;
|
|
159
|
+
- `yandex_docs_create_text` - создать текстовый документ на Яндекс Диске;
|
|
160
|
+
- `yandex_docs_read` - прочитать небольшой текстовый документ;
|
|
161
|
+
- `yandex_docs_share` - создать ссылку и QR-код на документ;
|
|
162
|
+
- `yandex_docs_rename` - переименовать документ;
|
|
163
|
+
- `yandex_docs_delete` - удалить документ;
|
|
148
164
|
- `yandex_contacts_status` - проверить доступ к Контактам;
|
|
149
165
|
- `yandex_contacts_list` - показать контакты;
|
|
150
166
|
- `yandex_contacts_search` - найти контакт по имени, email, телефону, организации, адресу или заметке;
|
|
@@ -166,7 +182,8 @@ QR-код:
|
|
|
166
182
|
- `yandex_contact_create_calendar_event` - создать встречу с контактом;
|
|
167
183
|
- `yandex_contact_create_telemost_event` - создать событие для Телемоста с контактом;
|
|
168
184
|
- `yandex_contact_from_public_entity` - создать контакт из открытого городского слоя;
|
|
169
|
-
- `
|
|
185
|
+
- `yandex_telemost_status` - проверить режим Телемоста;
|
|
186
|
+
- `yandex_telemost_create_event` - создать встречу: прямой Telemost API используется только если он доступен аккаунту; иначе создается календарное событие без выдуманной ссылки.
|
|
170
187
|
|
|
171
188
|
Регулярная проверка контактов:
|
|
172
189
|
|
|
@@ -233,7 +233,47 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
233
233
|
Работать с Яндекс Календарем:
|
|
234
234
|
|
|
235
235
|
- показать ближайшие события;
|
|
236
|
-
-
|
|
236
|
+
- показать список календарей;
|
|
237
|
+
- найти событие по названию, месту, описанию или дате;
|
|
238
|
+
- создать событие только после явного подтверждения;
|
|
239
|
+
- создать повторяющееся событие;
|
|
240
|
+
- перенести встречу на новую дату или время;
|
|
241
|
+
- изменить название, описание или место события;
|
|
242
|
+
- добавить напоминание;
|
|
243
|
+
- удалить событие по явной просьбе.
|
|
244
|
+
|
|
245
|
+
Примеры:
|
|
246
|
+
|
|
247
|
+
- `покажи яндекс календарь`;
|
|
248
|
+
- `найди событие собрание в яндекс календаре`;
|
|
249
|
+
- `создай событие название: прием врача завтра в 10:00`;
|
|
250
|
+
- `перенеси встречу прием врача на послезавтра в 11:00`;
|
|
251
|
+
- `добавь напоминание 20 минут к событию прием врача`;
|
|
252
|
+
- `удали событие прием врача`.
|
|
253
|
+
|
|
254
|
+
### yandex-docs
|
|
255
|
+
|
|
256
|
+
Работать с документами Яндекс 360 через Яндекс Диск:
|
|
257
|
+
|
|
258
|
+
- показать документы в папке `/IOLA/docs`;
|
|
259
|
+
- найти документ по названию;
|
|
260
|
+
- создать текстовый, Markdown или HTML-документ;
|
|
261
|
+
- прочитать небольшой текстовый документ;
|
|
262
|
+
- создать публичную ссылку и QR-код на документ;
|
|
263
|
+
- переименовать документ;
|
|
264
|
+
- удалить документ по явной просьбе.
|
|
265
|
+
|
|
266
|
+
Важно: CLI работает с документами как с файлами на Яндекс Диске. Он не подменяет веб-редактор Яндекс Документов и не обещает создание "нативного" документа, если Яндекс не дает стабильный API для этого действия.
|
|
267
|
+
|
|
268
|
+
Примеры:
|
|
269
|
+
|
|
270
|
+
- `покажи документы на Яндекс Диске`;
|
|
271
|
+
- `найди документ справка на Яндекс Диске`;
|
|
272
|
+
- `создай документ яндекс 360 название: заметка текст: купить лекарства`;
|
|
273
|
+
- `прочитай документ /IOLA/docs/заметка.md`;
|
|
274
|
+
- `сделай ссылку на документ /IOLA/docs/заметка.md`;
|
|
275
|
+
- `переименуй документ /IOLA/docs/заметка.md в лекарства.md`;
|
|
276
|
+
- `удали документ /IOLA/docs/лекарства.md`.
|
|
237
277
|
|
|
238
278
|
### yandex-contacts
|
|
239
279
|
|
|
@@ -294,7 +334,13 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
294
334
|
|
|
295
335
|
### yandex-telemost
|
|
296
336
|
|
|
297
|
-
Подготовить встречу через
|
|
337
|
+
Подготовить встречу через Телемост:
|
|
338
|
+
|
|
339
|
+
- проверить режим Телемоста;
|
|
340
|
+
- попробовать создать ссылку через прямой API, если он доступен текущему аккаунту;
|
|
341
|
+
- если прямой API недоступен, создать календарное событие и честно сообщить, что ссылка Телемоста не была получена через API.
|
|
342
|
+
|
|
343
|
+
CLI не должен сам нажимать финальные кнопки в веб-интерфейсе Яндекса без явного действия пользователя и не должен выдумывать ссылку Телемоста.
|
|
298
344
|
|
|
299
345
|
## Yandex Connector backlog
|
|
300
346
|
|