@iola_adm/iola-cli 0.2.31 → 0.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -27,8 +27,15 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
27
27
|
- `yandex_mail_read` - прочитать письмо по UID; обычное чтение помечает письмо прочитанным.
|
|
28
28
|
- `yandex_mail_send` - отправить письмо.
|
|
29
29
|
- `yandex_mail_reply` - ответить на письмо по UID.
|
|
30
|
+
- `yandex_mail_forward` - переслать письмо.
|
|
30
31
|
- `yandex_mail_delete` - переместить письмо в корзину.
|
|
31
32
|
- `yandex_mail_mark` - пометить письмо прочитанным или непрочитанным.
|
|
33
|
+
- `yandex_mail_save_to_disk` - сохранить письмо на Яндекс Диск в Markdown.
|
|
34
|
+
- `yandex_mail_create_calendar_event` - создать событие календаря из письма.
|
|
35
|
+
- `yandex_mail_sender_to_contact` - добавить отправителя письма в контакты.
|
|
36
|
+
- `yandex_mail_city_context` - найти в письме школы/детские сады/ИНН и подтянуть открытые городские слои.
|
|
37
|
+
- `yandex_mail_map_addresses` - найти адреса в письме и дать ссылки на Яндекс.Карты.
|
|
38
|
+
- `yandex_mail_create_task` - создать локальную задачу по письму.
|
|
32
39
|
- Автоопрос почты включается командой `iola yandex mail-watch on --minutes 5`, выключается `iola yandex mail-watch off`, ручная проверка `iola yandex mail-watch tick`.
|
|
33
40
|
- `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
|
|
34
41
|
- `yandex_calendar_list` - показать события.
|
|
@@ -36,13 +43,15 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
36
43
|
- `yandex_contacts_status` - проверить доступ к Яндекс Контактам.
|
|
37
44
|
- `yandex_contacts_list` - показать контакты.
|
|
38
45
|
- `yandex_contacts_search` - найти контакт.
|
|
46
|
+
- `yandex_contacts_create` - создать новый контакт.
|
|
39
47
|
- `yandex_contacts_add_email` - добавить email в существующий контакт.
|
|
40
48
|
- `yandex_telemost_create_event` - создать календарное событие для встречи.
|
|
41
49
|
|
|
42
50
|
Безопасность:
|
|
43
51
|
|
|
44
52
|
- Для отправки письма, ответа на письмо, удаления письма, удаления файлов, публикации ссылки и создания событий нужен явный запрос пользователя и `confirm=true`.
|
|
45
|
-
-
|
|
53
|
+
- Для пересылки письма, создания контакта, создания события из письма и добавления отправителя в контакты также нужен явный запрос пользователя и `confirm=true`.
|
|
54
|
+
- Не отправляй письма, не отвечай на письма, не пересылай письма, не создавай контакты/события, не удаляй письма/файлы и не публикуй ссылки по косвенному намерению.
|
|
46
55
|
- Список писем и поиск не должны помечать письма прочитанными. Чтение письма по просьбе пользователя помечает письмо прочитанным.
|
|
47
56
|
- Если пользователь просит отправить письмо по имени контакта, сначала ищи контакт. Если контактов несколько, покажи варианты и попроси уточнить. Если контакта нет или у него нет email, скажи прямо.
|
|
48
57
|
- Если пользователь отправил письмо на введенный email и указал имя контакта, можно предложить добавить email в найденный контакт без email.
|
package/src/cli.js
CHANGED
|
@@ -170,14 +170,22 @@ const YANDEX_TOOLS = [
|
|
|
170
170
|
"yandex_mail_read",
|
|
171
171
|
"yandex_mail_send",
|
|
172
172
|
"yandex_mail_reply",
|
|
173
|
+
"yandex_mail_forward",
|
|
173
174
|
"yandex_mail_delete",
|
|
174
175
|
"yandex_mail_mark",
|
|
176
|
+
"yandex_mail_save_to_disk",
|
|
177
|
+
"yandex_mail_create_calendar_event",
|
|
178
|
+
"yandex_mail_sender_to_contact",
|
|
179
|
+
"yandex_mail_city_context",
|
|
180
|
+
"yandex_mail_map_addresses",
|
|
181
|
+
"yandex_mail_create_task",
|
|
175
182
|
"yandex_calendar_status",
|
|
176
183
|
"yandex_calendar_create_event",
|
|
177
184
|
"yandex_calendar_list",
|
|
178
185
|
"yandex_contacts_status",
|
|
179
186
|
"yandex_contacts_list",
|
|
180
187
|
"yandex_contacts_search",
|
|
188
|
+
"yandex_contacts_create",
|
|
181
189
|
"yandex_contacts_add_email",
|
|
182
190
|
"yandex_telemost_create_event",
|
|
183
191
|
];
|
|
@@ -4007,14 +4015,22 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
4007
4015
|
if (tool === "yandex_mail_read") return yandexMailRead(args.uid || args.id, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: args.markSeen !== false });
|
|
4008
4016
|
if (tool === "yandex_mail_send") return yandexMailSend(args);
|
|
4009
4017
|
if (tool === "yandex_mail_reply") return yandexMailReply(args);
|
|
4018
|
+
if (tool === "yandex_mail_forward") return yandexMailForward(args);
|
|
4010
4019
|
if (tool === "yandex_mail_delete") return yandexMailDelete(args.uid || args.id, { ...args, mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX") });
|
|
4011
4020
|
if (tool === "yandex_mail_mark") return yandexMailMark(args.uid || args.id, args.seen !== false && args.unread !== true, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX") });
|
|
4021
|
+
if (tool === "yandex_mail_save_to_disk") return yandexMailSaveToDisk(args.uid || args.id, args);
|
|
4022
|
+
if (tool === "yandex_mail_create_calendar_event") return yandexMailCreateCalendarEvent(args.uid || args.id, args);
|
|
4023
|
+
if (tool === "yandex_mail_sender_to_contact") return yandexMailSenderToContact(args.uid || args.id, args);
|
|
4024
|
+
if (tool === "yandex_mail_city_context") return yandexMailCityContext(args.uid || args.id, args);
|
|
4025
|
+
if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
|
|
4026
|
+
if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
|
|
4012
4027
|
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
4013
4028
|
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
4014
4029
|
if (tool === "yandex_calendar_list") return yandexCalendarList(args);
|
|
4015
4030
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
4016
4031
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
4017
4032
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
4033
|
+
if (tool === "yandex_contacts_create") return yandexContactsCreate(args);
|
|
4018
4034
|
if (tool === "yandex_contacts_add_email") return yandexContactsAddEmail(args.query || args.name || "", args.email, args);
|
|
4019
4035
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
4020
4036
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
@@ -4255,6 +4271,123 @@ async function yandexMailReply(args = {}) {
|
|
|
4255
4271
|
return { ...result, replyToUid: Number(uid), originalFrom: original.from };
|
|
4256
4272
|
}
|
|
4257
4273
|
|
|
4274
|
+
async function yandexMailForward(args = {}) {
|
|
4275
|
+
if (!args.confirm) throw new Error("Для пересылки письма нужен аргумент confirm=true.");
|
|
4276
|
+
const uid = args.uid || args.id;
|
|
4277
|
+
const to = Array.isArray(args.to) ? args.to : String(args.to || "").split(/[;,]/).map((item) => item.trim()).filter(Boolean);
|
|
4278
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4279
|
+
if (!to.length) throw new Error("Получатель пересылки не указан.");
|
|
4280
|
+
const original = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: true });
|
|
4281
|
+
if (!original || original.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4282
|
+
const text = [
|
|
4283
|
+
args.text || args.comment || "",
|
|
4284
|
+
"",
|
|
4285
|
+
"---------- Пересланное письмо ----------",
|
|
4286
|
+
`От: ${original.from || "-"}`,
|
|
4287
|
+
`Дата: ${original.date || "-"}`,
|
|
4288
|
+
`Тема: ${original.subject || "(без темы)"}`,
|
|
4289
|
+
"",
|
|
4290
|
+
original.snippet || "",
|
|
4291
|
+
].join("\n").trim();
|
|
4292
|
+
return yandexMailSend({ to, subject: `Fwd: ${original.subject || "(без темы)"}`, text, confirm: true });
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
async function yandexMailSaveToDisk(uid, args = {}) {
|
|
4296
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4297
|
+
const row = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: args.markSeen !== false });
|
|
4298
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4299
|
+
const safeSubject = slugForFile(row.subject || `mail-${uid}`).slice(0, 80);
|
|
4300
|
+
const remotePath = args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/Почта/mail-${uid}-${safeSubject}.md`;
|
|
4301
|
+
const text = [
|
|
4302
|
+
`# ${row.subject || "(без темы)"}`,
|
|
4303
|
+
"",
|
|
4304
|
+
`- UID: ${row.uid}`,
|
|
4305
|
+
`- От: ${row.from || "-"}`,
|
|
4306
|
+
`- Дата: ${row.date || "-"}`,
|
|
4307
|
+
"",
|
|
4308
|
+
row.snippet || "",
|
|
4309
|
+
].join("\n");
|
|
4310
|
+
const saved = await yandexDiskSaveText(text, remotePath);
|
|
4311
|
+
return { ...saved, uid: row.uid, subject: row.subject };
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
async function yandexMailCreateCalendarEvent(uid, args = {}) {
|
|
4315
|
+
if (!args.confirm) throw new Error("Для создания события из письма нужен аргумент confirm=true.");
|
|
4316
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4317
|
+
const row = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: true });
|
|
4318
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4319
|
+
const detected = extractDateTimeFromText(`${row.subject}\n${row.snippet}`);
|
|
4320
|
+
const start = args.start || detected.start || new Date(Date.now() + 3600000).toISOString();
|
|
4321
|
+
const end = args.end || detected.end || new Date(new Date(start).getTime() + 3600000).toISOString();
|
|
4322
|
+
return yandexCalendarCreateEvent({
|
|
4323
|
+
title: args.title || row.subject || `Письмо #${uid}`,
|
|
4324
|
+
description: [`Создано из письма #${uid}.`, `От: ${row.from || "-"}`, "", row.snippet || ""].join("\n"),
|
|
4325
|
+
location: args.location || detected.location || "",
|
|
4326
|
+
start,
|
|
4327
|
+
end,
|
|
4328
|
+
confirm: true,
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
async function yandexMailSenderToContact(uid, args = {}) {
|
|
4333
|
+
if (!args.confirm) throw new Error("Для добавления отправителя в контакты нужен аргумент confirm=true.");
|
|
4334
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4335
|
+
const row = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: false });
|
|
4336
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4337
|
+
const email = extractEmailAddress(row.from);
|
|
4338
|
+
const name = extractDisplayName(row.from) || email;
|
|
4339
|
+
if (!email) throw new Error("В отправителе письма не найден email.");
|
|
4340
|
+
return yandexContactsCreate({ name, email, confirm: true });
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
async function yandexMailCityContext(uid, args = {}) {
|
|
4344
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4345
|
+
const row = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: false });
|
|
4346
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4347
|
+
const text = `${row.subject}\n${row.snippet}`;
|
|
4348
|
+
const queries = extractEducationQueriesFromText(text);
|
|
4349
|
+
const results = [];
|
|
4350
|
+
for (const query of queries) {
|
|
4351
|
+
results.push(...searchLocalRecords(query, { dataset: "all", limit: 5, fts: true }));
|
|
4352
|
+
}
|
|
4353
|
+
const unique = dedupeBy(results, (item) => `${item.dataset || ""}:${item.inn || item.name}`);
|
|
4354
|
+
return unique.slice(0, Number(args.limit || 10)).map((item) => ({
|
|
4355
|
+
name: item.name,
|
|
4356
|
+
inn: item.inn,
|
|
4357
|
+
address: item.address,
|
|
4358
|
+
phone: item.phone,
|
|
4359
|
+
email: item.email,
|
|
4360
|
+
website: item.website,
|
|
4361
|
+
dataset: item.dataset,
|
|
4362
|
+
}));
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
async function yandexMailMapAddresses(uid, args = {}) {
|
|
4366
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4367
|
+
const row = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: false });
|
|
4368
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4369
|
+
const addresses = extractLikelyAddresses(`${row.subject}\n${row.snippet}`).slice(0, Number(args.limit || 5));
|
|
4370
|
+
const rows = [];
|
|
4371
|
+
for (const address of addresses) {
|
|
4372
|
+
const point = await geocodeCached(address).catch(() => null);
|
|
4373
|
+
rows.push({
|
|
4374
|
+
address,
|
|
4375
|
+
resolved: point?.address || "",
|
|
4376
|
+
map: point?.lat && point?.lon ? `https://yandex.ru/maps/?pt=${point.lon},${point.lat}&z=16&l=map` : "",
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
return rows;
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
async function yandexMailCreateTask(uid, args = {}) {
|
|
4383
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4384
|
+
const row = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: false });
|
|
4385
|
+
if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4386
|
+
const title = args.title || `Ответить/разобрать письмо #${uid}: ${row.subject || "(без темы)"}`;
|
|
4387
|
+
const id = addTask(title, `ask "прочитай письмо #${uid}"`);
|
|
4388
|
+
return { id, title, uid: Number(uid), status: "open" };
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4258
4391
|
async function yandexMailSend(args = {}) {
|
|
4259
4392
|
if (!args.confirm) throw new Error("Для отправки письма нужен аргумент confirm=true.");
|
|
4260
4393
|
const to = Array.isArray(args.to) ? args.to : String(args.to || "").split(/[;,]/).map((item) => item.trim()).filter(Boolean);
|
|
@@ -4715,6 +4848,33 @@ async function yandexContactsAddEmail(query, email, args = {}) {
|
|
|
4715
4848
|
return { status: "updated", name: contact.name, email };
|
|
4716
4849
|
}
|
|
4717
4850
|
|
|
4851
|
+
async function yandexContactsCreate(args = {}) {
|
|
4852
|
+
if (!args.confirm) throw new Error("Для создания контакта нужен аргумент confirm=true.");
|
|
4853
|
+
const name = String(args.name || args.email || "").trim();
|
|
4854
|
+
const email = String(args.email || "").trim();
|
|
4855
|
+
if (!name) throw new Error("Имя контакта обязательно.");
|
|
4856
|
+
if (!email || !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email)) throw new Error("Корректный email обязателен.");
|
|
4857
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4858
|
+
const baseUrl = await yandexContactsBaseUrl(token);
|
|
4859
|
+
const uid = `${randomUUID()}@iola-cli`;
|
|
4860
|
+
const card = [
|
|
4861
|
+
"BEGIN:VCARD",
|
|
4862
|
+
"VERSION:3.0",
|
|
4863
|
+
`UID:${uid}`,
|
|
4864
|
+
`FN:${escapeVcardValue(name)}`,
|
|
4865
|
+
`EMAIL;TYPE=INTERNET:${email}`,
|
|
4866
|
+
"END:VCARD",
|
|
4867
|
+
"",
|
|
4868
|
+
].join("\r\n");
|
|
4869
|
+
await yandexDavRequest(new URL(`${encodeURIComponent(uid)}.vcf`, baseUrl).toString(), token, {
|
|
4870
|
+
method: "PUT",
|
|
4871
|
+
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4872
|
+
body: card,
|
|
4873
|
+
timeout: 45000,
|
|
4874
|
+
});
|
|
4875
|
+
return { status: "created", name, email, uid };
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4718
4878
|
function upsertVcardEmail(card, email, options = {}) {
|
|
4719
4879
|
const text = String(card || "").replace(/\r/g, "").trim();
|
|
4720
4880
|
if (!text.includes("BEGIN:VCARD")) throw new Error("Контакт не похож на vCard.");
|
|
@@ -4725,6 +4885,10 @@ function upsertVcardEmail(card, email, options = {}) {
|
|
|
4725
4885
|
return text.replace(/\nEND:VCARD/iu, `\nEMAIL;TYPE=INTERNET:${email}\nEND:VCARD`);
|
|
4726
4886
|
}
|
|
4727
4887
|
|
|
4888
|
+
function escapeVcardValue(value) {
|
|
4889
|
+
return String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4728
4892
|
function contactMatchesQuery(contact, normalizedQuery) {
|
|
4729
4893
|
const contactText = normalizeContactLookupText(`${contact.name || ""} ${contact.email || ""}`);
|
|
4730
4894
|
if (!contactText || !normalizedQuery) return false;
|
|
@@ -10040,6 +10204,51 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
10040
10204
|
const result = await yandexMailReply({ ...reply, confirm: true });
|
|
10041
10205
|
return `Ответ отправлен на письмо #${result.replyToUid}: ${result.to.join(", ")}. Тема: ${result.subject}.`;
|
|
10042
10206
|
}
|
|
10207
|
+
if (/(перешли|переслать|перешли\s+письмо|fwd|forward)/iu.test(normalized)) {
|
|
10208
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10209
|
+
const to = [...String(question || "").matchAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu)].map((match) => match[0]);
|
|
10210
|
+
if (!uid || !to.length) return "Для пересылки укажите письмо и получателя. Пример: перешли письмо #2382 user@example.com.";
|
|
10211
|
+
const result = await yandexMailForward({ uid, to, confirm: true });
|
|
10212
|
+
return `Письмо переслано: ${result.to.join(", ")}. Тема: ${result.subject}.`;
|
|
10213
|
+
}
|
|
10214
|
+
if (/(сохрани|запиши).{0,60}(диск|яндекс.?диск|облак)/iu.test(normalized)) {
|
|
10215
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10216
|
+
if (!uid) return "Какое письмо сохранить на Диск? Укажите номер из списка или UID.";
|
|
10217
|
+
const result = await yandexMailSaveToDisk(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
|
|
10218
|
+
return `Письмо #${uid} сохранено на Яндекс Диск: ${result.remote || result.path}.`;
|
|
10219
|
+
}
|
|
10220
|
+
if (/(создай|добавь).{0,40}(событи|встреч|календар)/iu.test(normalized)) {
|
|
10221
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10222
|
+
if (!uid) return "Из какого письма создать событие? Укажите номер из списка или UID.";
|
|
10223
|
+
const result = await yandexMailCreateCalendarEvent(uid, { mailbox: extractYandexMailboxName(question) || "INBOX", confirm: true });
|
|
10224
|
+
return `Событие создано в Яндекс Календаре: ${result.title || result.uid}.`;
|
|
10225
|
+
}
|
|
10226
|
+
if (/(добавь|создай|сохрани).{0,50}(отправител|автор|контакт)/iu.test(normalized)) {
|
|
10227
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10228
|
+
if (!uid) return "Из какого письма добавить отправителя в контакты? Укажите номер из списка или UID.";
|
|
10229
|
+
const result = await yandexMailSenderToContact(uid, { mailbox: extractYandexMailboxName(question) || "INBOX", confirm: true });
|
|
10230
|
+
return `Контакт создан: ${result.name}, ${result.email}.`;
|
|
10231
|
+
}
|
|
10232
|
+
if (/(школ|сад|детсад|инн|городск|сло[йи]|йошкар)/iu.test(normalized) && /(письм|письма|письме)/iu.test(normalized)) {
|
|
10233
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10234
|
+
if (!uid) return "По какому письму проверить городские слои? Укажите номер из списка или UID.";
|
|
10235
|
+
const rows = await yandexMailCityContext(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
|
|
10236
|
+
if (!rows.length) return "В письме не нашел совпадений со слоями школ и детских садов.";
|
|
10237
|
+
return ["Нашел в городских слоях:", ...rows.map((row, index) => `${index + 1}. ${row.name}${row.inn ? `, ИНН ${row.inn}` : ""}${row.address ? `, ${row.address}` : ""}`)].join("\n");
|
|
10238
|
+
}
|
|
10239
|
+
if (/(адрес|карт|гео|место|где)/iu.test(normalized) && /(письм|письма|письме)/iu.test(normalized)) {
|
|
10240
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10241
|
+
if (!uid) return "Из какого письма взять адрес? Укажите номер из списка или UID.";
|
|
10242
|
+
const rows = await yandexMailMapAddresses(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
|
|
10243
|
+
if (!rows.length) return "В письме не нашел адресов для карты.";
|
|
10244
|
+
return ["Адреса из письма:", ...rows.map((row, index) => `${index + 1}. ${row.resolved || row.address}${row.map ? `\n${row.map}` : ""}`)].join("\n");
|
|
10245
|
+
}
|
|
10246
|
+
if (/(создай|добавь).{0,30}(задач|напомин)/iu.test(normalized)) {
|
|
10247
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10248
|
+
if (!uid) return "По какому письму создать задачу? Укажите номер из списка или UID.";
|
|
10249
|
+
const result = await yandexMailCreateTask(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
|
|
10250
|
+
return `Задача создана #${result.id}: ${result.title}.`;
|
|
10251
|
+
}
|
|
10043
10252
|
if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) {
|
|
10044
10253
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10045
10254
|
if (!uid) return "Какое письмо удалить? Укажите номер из списка или UID, например: удали письмо #2382.";
|
|
@@ -10309,6 +10518,79 @@ function formatYandexMailRead(row) {
|
|
|
10309
10518
|
].filter((line) => line !== "").join("\n");
|
|
10310
10519
|
}
|
|
10311
10520
|
|
|
10521
|
+
function slugForFile(value) {
|
|
10522
|
+
return String(value || "")
|
|
10523
|
+
.toLocaleLowerCase("ru-RU")
|
|
10524
|
+
.replace(/[^\p{L}\p{N}._-]+/gu, "-")
|
|
10525
|
+
.replace(/-+/g, "-")
|
|
10526
|
+
.replace(/^-|-$/g, "")
|
|
10527
|
+
|| "item";
|
|
10528
|
+
}
|
|
10529
|
+
|
|
10530
|
+
function extractDisplayName(value) {
|
|
10531
|
+
const text = decodeMimeHeader(String(value || "").trim());
|
|
10532
|
+
return text.match(/^(.+?)\s*<[^<>]+>/u)?.[1]?.replace(/^["']|["']$/g, "").trim() || "";
|
|
10533
|
+
}
|
|
10534
|
+
|
|
10535
|
+
function extractDateTimeFromText(value) {
|
|
10536
|
+
const text = String(value || "").toLocaleLowerCase("ru-RU");
|
|
10537
|
+
let date = null;
|
|
10538
|
+
if (/завтра/u.test(text)) date = new Date(Date.now() + 86400000);
|
|
10539
|
+
if (/послезавтра/u.test(text)) date = new Date(Date.now() + 2 * 86400000);
|
|
10540
|
+
const explicit = text.match(/(\d{1,2})[.\-/](\d{1,2})(?:[.\-/](\d{2,4}))?/u);
|
|
10541
|
+
if (explicit) {
|
|
10542
|
+
const year = explicit[3] ? Number(explicit[3].length === 2 ? `20${explicit[3]}` : explicit[3]) : new Date().getFullYear();
|
|
10543
|
+
date = new Date(year, Number(explicit[2]) - 1, Number(explicit[1]), 9, 0, 0);
|
|
10544
|
+
}
|
|
10545
|
+
const time = text.match(/(?:в\s*)?(\d{1,2})[:.](\d{2})/u) || text.match(/(?:в\s+)(\d{1,2})\s*(?:час|ч\b)?/u);
|
|
10546
|
+
if (!date && time) date = new Date();
|
|
10547
|
+
if (date && time) {
|
|
10548
|
+
date.setHours(Number(time[1]), Number(time[2] || 0), 0, 0);
|
|
10549
|
+
}
|
|
10550
|
+
if (!date) return {};
|
|
10551
|
+
const start = date.toISOString();
|
|
10552
|
+
const end = new Date(date.getTime() + 3600000).toISOString();
|
|
10553
|
+
const location = extractLikelyAddresses(value)[0] || "";
|
|
10554
|
+
return { start, end, location };
|
|
10555
|
+
}
|
|
10556
|
+
|
|
10557
|
+
function extractEducationQueriesFromText(value) {
|
|
10558
|
+
const text = String(value || "");
|
|
10559
|
+
const queries = [];
|
|
10560
|
+
for (const match of text.matchAll(/(?:школ[ауыи]?|сош|гимнази[яи]|лице[йя])\s*№?\s*(\d{1,3})/giu)) queries.push(`школа ${match[1]}`);
|
|
10561
|
+
for (const match of text.matchAll(/(?:детск\w*\s+сад|детсад|садик)\s*№?\s*(\d{1,3})/giu)) queries.push(`детский сад ${match[1]}`);
|
|
10562
|
+
for (const match of text.matchAll(/\bинн\s*(\d{10,12})\b/giu)) queries.push(match[1]);
|
|
10563
|
+
return [...new Set(queries)];
|
|
10564
|
+
}
|
|
10565
|
+
|
|
10566
|
+
function extractLikelyAddresses(value) {
|
|
10567
|
+
const text = String(value || "").replace(/\s+/g, " ");
|
|
10568
|
+
const rows = [];
|
|
10569
|
+
const patterns = [
|
|
10570
|
+
/(?:адрес|по адресу|место)\s*:?\s*([^.!?\n]{8,120})/giu,
|
|
10571
|
+
/((?:ул\.?|улица|проспект|пр-т|бульвар|пер\.?|переулок)\s+[^.!?\n,]{3,80}(?:,\s*(?:д\.?\s*)?\d+[а-яa-z]?)?)/giu,
|
|
10572
|
+
];
|
|
10573
|
+
for (const pattern of patterns) {
|
|
10574
|
+
for (const match of text.matchAll(pattern)) {
|
|
10575
|
+
const address = String(match[1] || "").trim().replace(/[;,]+$/u, "");
|
|
10576
|
+
if (address.length >= 8 && !/https?:|www\.|@/iu.test(address) && /(ул\.?|улица|проспект|пр-т|бульвар|пер\.?|переулок|дом|д\.|\d)/iu.test(address)) rows.push(address);
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
return [...new Set(rows)].slice(0, 10);
|
|
10580
|
+
}
|
|
10581
|
+
|
|
10582
|
+
function dedupeBy(rows, keyFn) {
|
|
10583
|
+
const seen = new Set();
|
|
10584
|
+
const result = [];
|
|
10585
|
+
for (const row of rows) {
|
|
10586
|
+
const key = keyFn(row);
|
|
10587
|
+
if (seen.has(key)) continue;
|
|
10588
|
+
seen.add(key);
|
|
10589
|
+
result.push(row);
|
|
10590
|
+
}
|
|
10591
|
+
return result;
|
|
10592
|
+
}
|
|
10593
|
+
|
|
10312
10594
|
async function buildCloudDirectAnswer(question) {
|
|
10313
10595
|
if (!isCloudQuestion(question)) return "";
|
|
10314
10596
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
@@ -10918,8 +11200,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
10918
11200
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
10919
11201
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
10920
11202
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
10921
|
-
"Yandex tools: yandex_identity_me {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_save_text {path,text}, 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_calendar_list {start,end}, yandex_contacts_search {query}.",
|
|
10922
|
-
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_delete, yandex_mail_send, yandex_mail_reply, yandex_mail_delete, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
11203
|
+
"Yandex tools: yandex_identity_me {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_save_text {path,text}, 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_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_contacts_search {query}.",
|
|
11204
|
+
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_delete, 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_add_email, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
10923
11205
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
10924
11206
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
10925
11207
|
`Вопрос: ${question}`,
|
package/test/smoke-test.js
CHANGED
|
@@ -94,6 +94,13 @@ assertIncludes(cliSource, "yandexMailWatchTick", "Yandex mail should support man
|
|
|
94
94
|
assertIncludes(cliSource, "resolveYandexMailRecipientFromContacts", "Yandex mail should resolve recipients from contacts");
|
|
95
95
|
assertIncludes(cliSource, "yandexContactsAddEmail", "Yandex contacts should support adding email to a contact");
|
|
96
96
|
assertIncludes(cliSource, "yandex_contacts_add_email", "Yandex contacts add email should be exposed as a tool");
|
|
97
|
+
assertIncludes(cliSource, "yandex_mail_save_to_disk", "Yandex mail should save messages to Yandex Disk");
|
|
98
|
+
assertIncludes(cliSource, "yandex_mail_create_calendar_event", "Yandex mail should create calendar events from messages");
|
|
99
|
+
assertIncludes(cliSource, "yandex_mail_city_context", "Yandex mail should connect messages with city data layers");
|
|
100
|
+
assertIncludes(cliSource, "yandex_mail_map_addresses", "Yandex mail should map addresses from messages");
|
|
101
|
+
assertIncludes(cliSource, "yandex_mail_create_task", "Yandex mail should create local tasks from messages");
|
|
102
|
+
assertIncludes(cliSource, "yandex_mail_forward", "Yandex mail should support forwarding messages");
|
|
103
|
+
assertIncludes(cliSource, "yandexContactsCreate", "Yandex contacts should support creating contacts");
|
|
97
104
|
assertIncludes(cliSource, "buildCasualDirectAnswer(question)", "Casual greetings should bypass external AI providers");
|
|
98
105
|
assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
|
|
99
106
|
if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
|
|
@@ -180,10 +180,17 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
180
180
|
- пометить письмо прочитанным или непрочитанным;
|
|
181
181
|
- отправить письмо только после явного запроса;
|
|
182
182
|
- ответить на письмо по UID или номеру из последнего списка;
|
|
183
|
+
- переслать письмо другому получателю;
|
|
183
184
|
- удалить письмо безопасно: переместить его в корзину, а не стирать безвозвратно.
|
|
184
185
|
- включить автоопрос новых писем, например каждые 5 минут;
|
|
185
186
|
- выключить автоопрос новых писем;
|
|
186
187
|
- вручную проверить новые письма через mail-watch tick;
|
|
188
|
+
- сохранить письмо на Яндекс Диск в Markdown;
|
|
189
|
+
- создать событие календаря из письма;
|
|
190
|
+
- добавить отправителя письма в контакты;
|
|
191
|
+
- найти в письме школу, детский сад или ИНН и подтянуть карточку из открытых городских слоев;
|
|
192
|
+
- найти адреса в письме и дать ссылки на Яндекс.Карты;
|
|
193
|
+
- создать локальную задачу по письму;
|
|
187
194
|
- отправить письмо по имени контакта из Яндекс Контактов;
|
|
188
195
|
- если найдено несколько однофамильцев, попросить уточнить получателя;
|
|
189
196
|
- если контакт найден, но email не указан, прямо сообщить об этом;
|