@iola_adm/iola-cli 0.2.30 → 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,20 +27,33 @@ 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` - создать локальную задачу по письму.
|
|
39
|
+
- Автоопрос почты включается командой `iola yandex mail-watch on --minutes 5`, выключается `iola yandex mail-watch off`, ручная проверка `iola yandex mail-watch tick`.
|
|
32
40
|
- `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
|
|
33
41
|
- `yandex_calendar_list` - показать события.
|
|
34
42
|
- `yandex_calendar_create_event` - создать событие.
|
|
35
43
|
- `yandex_contacts_status` - проверить доступ к Яндекс Контактам.
|
|
36
44
|
- `yandex_contacts_list` - показать контакты.
|
|
37
45
|
- `yandex_contacts_search` - найти контакт.
|
|
46
|
+
- `yandex_contacts_create` - создать новый контакт.
|
|
47
|
+
- `yandex_contacts_add_email` - добавить email в существующий контакт.
|
|
38
48
|
- `yandex_telemost_create_event` - создать календарное событие для встречи.
|
|
39
49
|
|
|
40
50
|
Безопасность:
|
|
41
51
|
|
|
42
52
|
- Для отправки письма, ответа на письмо, удаления письма, удаления файлов, публикации ссылки и создания событий нужен явный запрос пользователя и `confirm=true`.
|
|
43
|
-
-
|
|
53
|
+
- Для пересылки письма, создания контакта, создания события из письма и добавления отправителя в контакты также нужен явный запрос пользователя и `confirm=true`.
|
|
54
|
+
- Не отправляй письма, не отвечай на письма, не пересылай письма, не создавай контакты/события, не удаляй письма/файлы и не публикуй ссылки по косвенному намерению.
|
|
44
55
|
- Список писем и поиск не должны помечать письма прочитанными. Чтение письма по просьбе пользователя помечает письмо прочитанным.
|
|
56
|
+
- Если пользователь просит отправить письмо по имени контакта, сначала ищи контакт. Если контактов несколько, покажи варианты и попроси уточнить. Если контакта нет или у него нет email, скажи прямо.
|
|
57
|
+
- Если пользователь отправил письмо на введенный email и указал имя контакта, можно предложить добавить email в найденный контакт без email.
|
|
45
58
|
- Не выводи OAuth-токены, API-ключи, пароли и секреты.
|
|
46
59
|
- Если сервис не подключен, скажи запустить `iola yandex setup` или открыть `/master`.
|
package/src/cli.js
CHANGED
|
@@ -170,14 +170,23 @@ 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",
|
|
189
|
+
"yandex_contacts_add_email",
|
|
181
190
|
"yandex_telemost_create_event",
|
|
182
191
|
];
|
|
183
192
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS];
|
|
@@ -3308,6 +3317,11 @@ async function handleYandex(args) {
|
|
|
3308
3317
|
return;
|
|
3309
3318
|
}
|
|
3310
3319
|
|
|
3320
|
+
if (action === "mail-watch" || action === "mailwatch" || action === "watch-mail") {
|
|
3321
|
+
await handleYandexMailWatch([target, ...rest].filter(Boolean));
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3311
3325
|
if (action === "enable" || action === "disable") {
|
|
3312
3326
|
const services = [target, ...rest].filter((item) => item && !String(item).startsWith("--"));
|
|
3313
3327
|
if (services.length === 0) throw new Error("Укажите сервисы. Пример: iola yandex enable disk mail calendar");
|
|
@@ -3343,6 +3357,7 @@ async function handleYandex(args) {
|
|
|
3343
3357
|
iola yandex menu
|
|
3344
3358
|
iola yandex status|doctor
|
|
3345
3359
|
iola yandex services
|
|
3360
|
+
iola yandex mail-watch on|off|status|tick [--minutes 5]
|
|
3346
3361
|
iola yandex enable disk mail calendar
|
|
3347
3362
|
iola yandex disable mail
|
|
3348
3363
|
iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
|
|
@@ -3372,6 +3387,47 @@ function printYandexServices(options = {}) {
|
|
|
3372
3387
|
]);
|
|
3373
3388
|
}
|
|
3374
3389
|
|
|
3390
|
+
async function handleYandexMailWatch(args = []) {
|
|
3391
|
+
const [action = "status", ...rest] = args;
|
|
3392
|
+
const options = parseOptions(rest);
|
|
3393
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3394
|
+
const minutes = Math.max(1, Number(options.minutes || options.interval || rest.find((item) => /^\d+$/u.test(String(item))) || 5));
|
|
3395
|
+
const result = await yandexMailWatchEnable(minutes);
|
|
3396
|
+
console.log(`Автопроверка новых писем включена: каждые ${minutes} минут. Текущий последний UID: ${result.lastUid || "-"}.`);
|
|
3397
|
+
console.log("Для работы по расписанию должен запускаться cron tick: вручную, через daemon или Windows Task Scheduler.");
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3401
|
+
await yandexMailWatchDisable();
|
|
3402
|
+
console.log("Автопроверка новых писем выключена.");
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3406
|
+
const result = await yandexMailWatchTick(options);
|
|
3407
|
+
if (!result.enabled) {
|
|
3408
|
+
console.log("Автопроверка новых писем выключена.");
|
|
3409
|
+
} else if (!result.newMessages.length) {
|
|
3410
|
+
console.log("Новых писем нет.");
|
|
3411
|
+
} else {
|
|
3412
|
+
console.log(["Новые письма:", ...result.newMessages.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n"));
|
|
3413
|
+
}
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
if (action === "status" || action === "doctor") {
|
|
3417
|
+
const config = await loadConfig();
|
|
3418
|
+
const watch = config.yandex?.mailWatch || {};
|
|
3419
|
+
printKeyValue({
|
|
3420
|
+
enabled: watch.enabled ? "yes" : "no",
|
|
3421
|
+
minutes: watch.minutes || "-",
|
|
3422
|
+
mailbox: watch.mailbox || "INBOX",
|
|
3423
|
+
lastUid: watch.lastUid || "-",
|
|
3424
|
+
cron: listCronJobs().some((job) => job.command === "yandex mail-watch tick") ? "yes" : "no",
|
|
3425
|
+
});
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3375
3431
|
async function setupYandexConnector(args = []) {
|
|
3376
3432
|
const options = parseOptions(args);
|
|
3377
3433
|
const config = await loadConfig();
|
|
@@ -3959,14 +4015,23 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
3959
4015
|
if (tool === "yandex_mail_read") return yandexMailRead(args.uid || args.id, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: args.markSeen !== false });
|
|
3960
4016
|
if (tool === "yandex_mail_send") return yandexMailSend(args);
|
|
3961
4017
|
if (tool === "yandex_mail_reply") return yandexMailReply(args);
|
|
4018
|
+
if (tool === "yandex_mail_forward") return yandexMailForward(args);
|
|
3962
4019
|
if (tool === "yandex_mail_delete") return yandexMailDelete(args.uid || args.id, { ...args, mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX") });
|
|
3963
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);
|
|
3964
4027
|
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
3965
4028
|
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
3966
4029
|
if (tool === "yandex_calendar_list") return yandexCalendarList(args);
|
|
3967
4030
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
3968
4031
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
3969
4032
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
4033
|
+
if (tool === "yandex_contacts_create") return yandexContactsCreate(args);
|
|
4034
|
+
if (tool === "yandex_contacts_add_email") return yandexContactsAddEmail(args.query || args.name || "", args.email, args);
|
|
3970
4035
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
3971
4036
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
3972
4037
|
}
|
|
@@ -4087,6 +4152,52 @@ async function yandexMailSearch(query, options = {}) {
|
|
|
4087
4152
|
return rows.filter((row) => normalizeGeoText(`${row.from} ${row.subject} ${row.snippet}`).includes(normalized)).slice(0, Number(options.limit || 20));
|
|
4088
4153
|
}
|
|
4089
4154
|
|
|
4155
|
+
async function yandexMailWatchTick(options = {}) {
|
|
4156
|
+
const config = await loadConfig();
|
|
4157
|
+
const watch = config.yandex?.mailWatch || {};
|
|
4158
|
+
if (!watch.enabled && !options.force) return { enabled: false, newMessages: [] };
|
|
4159
|
+
const mailbox = watch.mailbox || "INBOX";
|
|
4160
|
+
const lastUid = Number(watch.lastUid || 0);
|
|
4161
|
+
const rows = await yandexMailList({ mailbox, limit: Number(options.limit || 30), unread: false });
|
|
4162
|
+
const newMessages = rows.filter((row) => Number(row.uid || 0) > lastUid).sort((left, right) => Number(left.uid || 0) - Number(right.uid || 0));
|
|
4163
|
+
const newestUid = Math.max(lastUid, ...rows.map((row) => Number(row.uid || 0)));
|
|
4164
|
+
await saveConfig({
|
|
4165
|
+
yandex: {
|
|
4166
|
+
...(config.yandex || {}),
|
|
4167
|
+
mailWatch: {
|
|
4168
|
+
...watch,
|
|
4169
|
+
enabled: watch.enabled !== false,
|
|
4170
|
+
mailbox,
|
|
4171
|
+
lastUid: newestUid,
|
|
4172
|
+
lastCheckedAt: new Date().toISOString(),
|
|
4173
|
+
},
|
|
4174
|
+
},
|
|
4175
|
+
});
|
|
4176
|
+
return { enabled: true, mailbox, lastUid, newestUid, newMessages };
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
async function yandexMailWatchEnable(minutes = 5) {
|
|
4180
|
+
const config = await loadConfig();
|
|
4181
|
+
const safeMinutes = Math.max(1, Number(minutes || 5));
|
|
4182
|
+
const latest = await yandexMailList({ mailbox: "INBOX", limit: 1, unread: false });
|
|
4183
|
+
const lastUid = latest[0]?.uid || 0;
|
|
4184
|
+
await saveConfig({
|
|
4185
|
+
yandex: {
|
|
4186
|
+
...(config.yandex || {}),
|
|
4187
|
+
mailWatch: { enabled: true, minutes: safeMinutes, mailbox: "INBOX", lastUid, updatedAt: new Date().toISOString() },
|
|
4188
|
+
},
|
|
4189
|
+
});
|
|
4190
|
+
await upsertCronJob(`каждые ${safeMinutes} минут`, "yandex mail-watch tick", { replaceCommand: true });
|
|
4191
|
+
return { enabled: true, minutes: safeMinutes, mailbox: "INBOX", lastUid };
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
async function yandexMailWatchDisable() {
|
|
4195
|
+
const config = await loadConfig();
|
|
4196
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), mailWatch: { ...(config.yandex?.mailWatch || {}), enabled: false, updatedAt: new Date().toISOString() } } });
|
|
4197
|
+
deleteCronJobsByCommand("yandex mail-watch tick");
|
|
4198
|
+
return { enabled: false };
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4090
4201
|
async function yandexMailRead(uid, options = {}) {
|
|
4091
4202
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
4092
4203
|
const { token, email } = await yandexMailCredentials();
|
|
@@ -4160,6 +4271,123 @@ async function yandexMailReply(args = {}) {
|
|
|
4160
4271
|
return { ...result, replyToUid: Number(uid), originalFrom: original.from };
|
|
4161
4272
|
}
|
|
4162
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
|
+
|
|
4163
4391
|
async function yandexMailSend(args = {}) {
|
|
4164
4392
|
if (!args.confirm) throw new Error("Для отправки письма нужен аргумент confirm=true.");
|
|
4165
4393
|
const to = Array.isArray(args.to) ? args.to : String(args.to || "").split(/[;,]/).map((item) => item.trim()).filter(Boolean);
|
|
@@ -4573,7 +4801,7 @@ async function yandexContactsList(args = {}) {
|
|
|
4573
4801
|
const rows = [];
|
|
4574
4802
|
for (const href of hrefs) {
|
|
4575
4803
|
const card = await yandexDavRequest(new URL(href, "https://carddav.yandex.ru/").toString(), token, { method: "GET", timeout: 30000 }).catch(() => "");
|
|
4576
|
-
rows.push(...parseVCards(card));
|
|
4804
|
+
rows.push(...parseVCards(card).map((row) => args.full ? { ...row, href, card } : row));
|
|
4577
4805
|
}
|
|
4578
4806
|
return rows.slice(0, Number(args.limit || 50));
|
|
4579
4807
|
}
|
|
@@ -4585,6 +4813,95 @@ async function yandexContactsSearch(query, args = {}) {
|
|
|
4585
4813
|
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.email} ${row.phone}`).includes(normalized)).slice(0, Number(args.limit || 20));
|
|
4586
4814
|
}
|
|
4587
4815
|
|
|
4816
|
+
async function resolveYandexMailRecipientFromContacts(query) {
|
|
4817
|
+
const normalized = normalizeContactLookupText(query);
|
|
4818
|
+
if (!normalized) return { status: "not-found", contacts: [] };
|
|
4819
|
+
const rows = await yandexContactsList({ limit: 300 });
|
|
4820
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, 10);
|
|
4821
|
+
if (!matches.length) return { status: "not-found", contacts: [] };
|
|
4822
|
+
const withEmail = matches.filter((row) => row.email);
|
|
4823
|
+
if (withEmail.length === 1) return { status: "ok", contact: withEmail[0] };
|
|
4824
|
+
if (withEmail.length > 1) return { status: "ambiguous", contacts: withEmail };
|
|
4825
|
+
return matches.length === 1
|
|
4826
|
+
? { status: "no-email", contact: matches[0] }
|
|
4827
|
+
: { status: "ambiguous", contacts: matches };
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
async function yandexContactsAddEmail(query, email, args = {}) {
|
|
4831
|
+
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
4832
|
+
if (!query) throw new Error("Укажите имя или часть имени контакта.");
|
|
4833
|
+
if (!email || !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(String(email))) throw new Error("Укажите корректный email.");
|
|
4834
|
+
const rows = await yandexContactsList({ limit: 500, full: true });
|
|
4835
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalizeContactLookupText(query))).slice(0, 10);
|
|
4836
|
+
if (!matches.length) return { status: "not-found", query };
|
|
4837
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", query, contacts: matches.map(({ card, ...row }) => row) };
|
|
4838
|
+
const contact = matches[0];
|
|
4839
|
+
if (contact.email && !args.overwrite) return { status: "has-email", contact: { name: contact.name, email: contact.email } };
|
|
4840
|
+
const updatedCard = upsertVcardEmail(contact.card, email, { overwrite: Boolean(args.overwrite) });
|
|
4841
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4842
|
+
await yandexDavRequest(new URL(contact.href, "https://carddav.yandex.ru/").toString(), token, {
|
|
4843
|
+
method: "PUT",
|
|
4844
|
+
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4845
|
+
body: updatedCard,
|
|
4846
|
+
timeout: 45000,
|
|
4847
|
+
});
|
|
4848
|
+
return { status: "updated", name: contact.name, email };
|
|
4849
|
+
}
|
|
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
|
+
|
|
4878
|
+
function upsertVcardEmail(card, email, options = {}) {
|
|
4879
|
+
const text = String(card || "").replace(/\r/g, "").trim();
|
|
4880
|
+
if (!text.includes("BEGIN:VCARD")) throw new Error("Контакт не похож на vCard.");
|
|
4881
|
+
if (/^EMAIL[^:]*:/imu.test(text)) {
|
|
4882
|
+
if (!options.overwrite) return text;
|
|
4883
|
+
return text.replace(/^EMAIL[^:]*:[^\n]*/imu, `EMAIL;TYPE=INTERNET:${email}`);
|
|
4884
|
+
}
|
|
4885
|
+
return text.replace(/\nEND:VCARD/iu, `\nEMAIL;TYPE=INTERNET:${email}\nEND:VCARD`);
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
function escapeVcardValue(value) {
|
|
4889
|
+
return String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
function contactMatchesQuery(contact, normalizedQuery) {
|
|
4893
|
+
const contactText = normalizeContactLookupText(`${contact.name || ""} ${contact.email || ""}`);
|
|
4894
|
+
if (!contactText || !normalizedQuery) return false;
|
|
4895
|
+
if (contactText.includes(normalizedQuery)) return true;
|
|
4896
|
+
const queryTokens = normalizedQuery.split(/\s+/u).filter(Boolean);
|
|
4897
|
+
const contactTokens = contactText.split(/\s+/u).filter(Boolean);
|
|
4898
|
+
return queryTokens.every((queryToken) => contactTokens.some((contactToken) => contactToken.startsWith(queryToken.slice(0, Math.max(4, Math.min(queryToken.length, 6)))) || queryToken.startsWith(contactToken.slice(0, Math.max(4, Math.min(contactToken.length, 6))))));
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
function normalizeContactLookupText(value) {
|
|
4902
|
+
return normalizeGeoText(String(value || "").replace(/\b(?:кому|контакт|письмо|сообщение)\b/giu, " ")).trim();
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4588
4905
|
async function yandexContactsBaseUrl(token) {
|
|
4589
4906
|
const root = "https://carddav.yandex.ru/";
|
|
4590
4907
|
const principalXml = await yandexDavRequest(root, token, {
|
|
@@ -9273,6 +9590,25 @@ function addCronJob(scheduleText, command) {
|
|
|
9273
9590
|
}
|
|
9274
9591
|
}
|
|
9275
9592
|
|
|
9593
|
+
async function upsertCronJob(scheduleText, command, options = {}) {
|
|
9594
|
+
initDatabase();
|
|
9595
|
+
const db = openDatabase();
|
|
9596
|
+
try {
|
|
9597
|
+
const existing = db.prepare("SELECT id FROM cron_jobs WHERE command = ? ORDER BY id DESC LIMIT 1").get(command);
|
|
9598
|
+
if (existing?.id) {
|
|
9599
|
+
db.prepare("UPDATE cron_jobs SET schedule_text = ?, enabled = 1 WHERE id = ?").run(scheduleText, existing.id);
|
|
9600
|
+
return Number(existing.id);
|
|
9601
|
+
}
|
|
9602
|
+
if (options.replaceCommand) {
|
|
9603
|
+
db.prepare("DELETE FROM cron_jobs WHERE command = ?").run(command);
|
|
9604
|
+
}
|
|
9605
|
+
const result = db.prepare("INSERT INTO cron_jobs(schedule_text, command) VALUES (?, ?)").run(scheduleText, command);
|
|
9606
|
+
return Number(result.lastInsertRowid);
|
|
9607
|
+
} finally {
|
|
9608
|
+
db.close();
|
|
9609
|
+
}
|
|
9610
|
+
}
|
|
9611
|
+
|
|
9276
9612
|
function deleteCronJob(id) {
|
|
9277
9613
|
initDatabase();
|
|
9278
9614
|
const db = openDatabase();
|
|
@@ -9283,6 +9619,16 @@ function deleteCronJob(id) {
|
|
|
9283
9619
|
}
|
|
9284
9620
|
}
|
|
9285
9621
|
|
|
9622
|
+
function deleteCronJobsByCommand(command) {
|
|
9623
|
+
initDatabase();
|
|
9624
|
+
const db = openDatabase();
|
|
9625
|
+
try {
|
|
9626
|
+
db.prepare("DELETE FROM cron_jobs WHERE command = ?").run(command);
|
|
9627
|
+
} finally {
|
|
9628
|
+
db.close();
|
|
9629
|
+
}
|
|
9630
|
+
}
|
|
9631
|
+
|
|
9286
9632
|
function dueCronJobs() {
|
|
9287
9633
|
initDatabase();
|
|
9288
9634
|
const db = openDatabase();
|
|
@@ -9829,6 +10175,20 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9829
10175
|
}
|
|
9830
10176
|
|
|
9831
10177
|
if (mailFollowup || /(почт|письм|email|e-mail|спам|чернов|отправлен|исходящ|корзин)/iu.test(normalized)) {
|
|
10178
|
+
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
10179
|
+
const minutes = Number(String(question || "").match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 5);
|
|
10180
|
+
await yandexMailWatchEnable(minutes);
|
|
10181
|
+
return `Автопроверка новых писем включена: каждые ${minutes} минут.`;
|
|
10182
|
+
}
|
|
10183
|
+
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
10184
|
+
await yandexMailWatchDisable();
|
|
10185
|
+
return "Автопроверка новых писем выключена.";
|
|
10186
|
+
}
|
|
10187
|
+
if (/(проверь|получи|есть).{0,40}(нов|свеж)/iu.test(normalized)) {
|
|
10188
|
+
const result = await yandexMailWatchTick({ force: true });
|
|
10189
|
+
if (!result.newMessages.length) return "Новых писем нет.";
|
|
10190
|
+
return ["Новые письма:", ...result.newMessages.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
10191
|
+
}
|
|
9832
10192
|
if (/(папк|ящик|mailbox|folder)/iu.test(normalized) && /(покажи|список|какие|есть)/iu.test(normalized)) {
|
|
9833
10193
|
const folders = await yandexMailFolders();
|
|
9834
10194
|
if (!folders.length) return "Папки Яндекс Почты не найдены.";
|
|
@@ -9844,6 +10204,51 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9844
10204
|
const result = await yandexMailReply({ ...reply, confirm: true });
|
|
9845
10205
|
return `Ответ отправлен на письмо #${result.replyToUid}: ${result.to.join(", ")}. Тема: ${result.subject}.`;
|
|
9846
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
|
+
}
|
|
9847
10252
|
if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) {
|
|
9848
10253
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
9849
10254
|
if (!uid) return "Какое письмо удалить? Укажите номер из списка или UID, например: удали письмо #2382.";
|
|
@@ -9861,11 +10266,34 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9861
10266
|
}
|
|
9862
10267
|
if (/(отправь|отправить|напиши|пошли)/iu.test(normalized) && !/(отправлен|исходящ)/iu.test(normalized)) {
|
|
9863
10268
|
const draft = parseYandexMailSendRequest(question);
|
|
10269
|
+
if (!draft.to.length && draft.contactQuery) {
|
|
10270
|
+
const contactResult = await resolveYandexMailRecipientFromContacts(draft.contactQuery);
|
|
10271
|
+
if (contactResult.status === "not-found") return `В Яндекс Контактах не нашел: ${draft.contactQuery}. Укажите email вручную.`;
|
|
10272
|
+
if (contactResult.status === "no-email") return `Контакт найден, но email не указан: ${contactResult.contact.name}. Укажите email вручную.`;
|
|
10273
|
+
if (contactResult.status === "ambiguous") {
|
|
10274
|
+
return [
|
|
10275
|
+
`Нашел несколько контактов для "${draft.contactQuery}". Уточните, кому отправить:`,
|
|
10276
|
+
...contactResult.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ", email не указан"}`),
|
|
10277
|
+
].join("\n");
|
|
10278
|
+
}
|
|
10279
|
+
draft.to = [contactResult.contact.email];
|
|
10280
|
+
}
|
|
9864
10281
|
if (!draft.to.length || !draft.text) {
|
|
9865
10282
|
return "Для отправки письма укажите получателя и текст. Пример: отправь письмо user@example.com тема: Привет текст: Проверка.";
|
|
9866
10283
|
}
|
|
9867
10284
|
const result = await yandexMailSend({ ...draft, confirm: true });
|
|
9868
|
-
|
|
10285
|
+
let contactNote = "";
|
|
10286
|
+
if (draft.contactQuery && result.to[0] && process.stdin.isTTY) {
|
|
10287
|
+
const contactResult = await resolveYandexMailRecipientFromContacts(draft.contactQuery).catch(() => null);
|
|
10288
|
+
if (contactResult?.status === "no-email") {
|
|
10289
|
+
const shouldAdd = await askYesNo(`Добавить ${result.to[0]} в контакт "${contactResult.contact.name}"? [y/N] `, false);
|
|
10290
|
+
if (shouldAdd) {
|
|
10291
|
+
const update = await yandexContactsAddEmail(draft.contactQuery, result.to[0], { confirm: true, selectFirst: true });
|
|
10292
|
+
contactNote = update.status === "updated" ? `\nEmail добавлен в контакт: ${update.name || draft.contactQuery}.` : "";
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
return `Письмо отправлено: ${result.to.join(", ")}. Тема: ${result.subject}.${contactNote}`;
|
|
9869
10297
|
}
|
|
9870
10298
|
if (/(сколько|количеств|есть\s+ли)/iu.test(normalized) && /непрочитан/iu.test(normalized)) {
|
|
9871
10299
|
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
@@ -9900,6 +10328,21 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9900
10328
|
}
|
|
9901
10329
|
|
|
9902
10330
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
10331
|
+
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(normalized)) {
|
|
10332
|
+
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
10333
|
+
const query = cleanupYandexContactEmailTarget(question, email);
|
|
10334
|
+
if (!email || !query) return "Укажите контакт и email. Пример: добавь email petrov@example.com к контакту Петров.";
|
|
10335
|
+
const result = await yandexContactsAddEmail(query, email, { confirm: true });
|
|
10336
|
+
if (result.status === "not-found") return `Контакт не найден: ${query}.`;
|
|
10337
|
+
if (result.status === "has-email") return `У контакта уже есть email: ${result.contact.name}, ${result.contact.email}.`;
|
|
10338
|
+
if (result.status === "ambiguous") {
|
|
10339
|
+
return [
|
|
10340
|
+
`Нашел несколько контактов для "${query}". Уточните контакт:`,
|
|
10341
|
+
...result.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ""}`),
|
|
10342
|
+
].join("\n");
|
|
10343
|
+
}
|
|
10344
|
+
return `Email добавлен в контакт: ${result.name || query}, ${result.email}.`;
|
|
10345
|
+
}
|
|
9903
10346
|
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
9904
10347
|
const result = await yandexContactsStatus();
|
|
9905
10348
|
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
@@ -10024,6 +10467,7 @@ function parseYandexMailSendRequest(question) {
|
|
|
10024
10467
|
const emails = [...text.matchAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu)].map((match) => match[0]);
|
|
10025
10468
|
const subjectMatch = text.match(/(?:тема|subject)\s*:\s*(.*?)(?=\s+(?:текст|body|сообщение)\s*:|$)/iu);
|
|
10026
10469
|
const bodyMatch = text.match(/(?:текст|body|сообщение)\s*:\s*(.*)$/iu);
|
|
10470
|
+
const contactMatch = text.match(/^(?:отправь|отправить|напиши|пошли)\s+(.+?)(?:\s+письмо|\s+сообщение|\s+текст\s*:|\s+напомни|\s+скажи|$)/iu);
|
|
10027
10471
|
const withoutCommand = text
|
|
10028
10472
|
.replace(/^(?:отправь|отправить|напиши|пошли)\s+(?:письмо\s+)?/iu, "")
|
|
10029
10473
|
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "")
|
|
@@ -10031,10 +10475,33 @@ function parseYandexMailSendRequest(question) {
|
|
|
10031
10475
|
return {
|
|
10032
10476
|
to: emails,
|
|
10033
10477
|
subject: (subjectMatch?.[1] || "Сообщение от IOLA CLI").trim(),
|
|
10034
|
-
text: (bodyMatch?.[1] || (!subjectMatch ? withoutCommand : "")).trim(),
|
|
10478
|
+
text: (bodyMatch?.[1] || cleanupYandexMailBodyText(!subjectMatch ? withoutCommand : "")).trim(),
|
|
10479
|
+
contactQuery: cleanupYandexContactQuery((contactMatch?.[1] || "").replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "")),
|
|
10035
10480
|
};
|
|
10036
10481
|
}
|
|
10037
10482
|
|
|
10483
|
+
function cleanupYandexMailBodyText(value) {
|
|
10484
|
+
return String(value || "")
|
|
10485
|
+
.replace(/^.+?\s+(?:письмо|сообщение)\s+/iu, "")
|
|
10486
|
+
.trim();
|
|
10487
|
+
}
|
|
10488
|
+
|
|
10489
|
+
function cleanupYandexContactQuery(value) {
|
|
10490
|
+
return String(value || "")
|
|
10491
|
+
.replace(/\b(?:письмо|сообщение|текст|напомни|скажи|ему|ей)\b/giu, " ")
|
|
10492
|
+
.replace(/\s+/g, " ")
|
|
10493
|
+
.trim();
|
|
10494
|
+
}
|
|
10495
|
+
|
|
10496
|
+
function cleanupYandexContactEmailTarget(question, email) {
|
|
10497
|
+
return String(question || "")
|
|
10498
|
+
.replace(email || "", " ")
|
|
10499
|
+
.replace(/\b(?:добавь|добавить|запиши|сохрани|email|e-mail|почту|почта|к|ко|контакту|контакт)\b/giu, " ")
|
|
10500
|
+
.replace(/[,:;.!?]+/gu, " ")
|
|
10501
|
+
.replace(/\s+/g, " ")
|
|
10502
|
+
.trim();
|
|
10503
|
+
}
|
|
10504
|
+
|
|
10038
10505
|
function formatYandexMailSummary(row) {
|
|
10039
10506
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
10040
10507
|
}
|
|
@@ -10051,6 +10518,79 @@ function formatYandexMailRead(row) {
|
|
|
10051
10518
|
].filter((line) => line !== "").join("\n");
|
|
10052
10519
|
}
|
|
10053
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
|
+
|
|
10054
10594
|
async function buildCloudDirectAnswer(question) {
|
|
10055
10595
|
if (!isCloudQuestion(question)) return "";
|
|
10056
10596
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
@@ -10660,8 +11200,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
10660
11200
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
10661
11201
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
10662
11202
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
10663
|
-
"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}.",
|
|
10664
|
-
"Опасные 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.",
|
|
10665
11205
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
10666
11206
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
10667
11207
|
`Вопрос: ${question}`,
|
package/test/smoke-test.js
CHANGED
|
@@ -89,6 +89,18 @@ assertIncludes(cliSource, "yandex_mail_delete", "Yandex mail should expose safe
|
|
|
89
89
|
assertIncludes(cliSource, "yandex_mail_mark", "Yandex mail should expose read/unread mark as a tool");
|
|
90
90
|
assertIncludes(cliSource, "decodeImapModifiedUtf7", "Yandex mail folders should decode IMAP modified UTF-7 names");
|
|
91
91
|
assertIncludes(cliSource, "encodeImapModifiedUtf7", "Yandex mail should select non-ASCII folders via IMAP modified UTF-7");
|
|
92
|
+
assertIncludes(cliSource, "yandexMailWatchEnable", "Yandex mail should support scheduled new-mail polling");
|
|
93
|
+
assertIncludes(cliSource, "yandexMailWatchTick", "Yandex mail should support manual new-mail polling");
|
|
94
|
+
assertIncludes(cliSource, "resolveYandexMailRecipientFromContacts", "Yandex mail should resolve recipients from contacts");
|
|
95
|
+
assertIncludes(cliSource, "yandexContactsAddEmail", "Yandex contacts should support adding email to a contact");
|
|
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");
|
|
92
104
|
assertIncludes(cliSource, "buildCasualDirectAnswer(question)", "Casual greetings should bypass external AI providers");
|
|
93
105
|
assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
|
|
94
106
|
if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
|
|
@@ -180,10 +180,26 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
180
180
|
- пометить письмо прочитанным или непрочитанным;
|
|
181
181
|
- отправить письмо только после явного запроса;
|
|
182
182
|
- ответить на письмо по UID или номеру из последнего списка;
|
|
183
|
+
- переслать письмо другому получателю;
|
|
183
184
|
- удалить письмо безопасно: переместить его в корзину, а не стирать безвозвратно.
|
|
185
|
+
- включить автоопрос новых писем, например каждые 5 минут;
|
|
186
|
+
- выключить автоопрос новых писем;
|
|
187
|
+
- вручную проверить новые письма через mail-watch tick;
|
|
188
|
+
- сохранить письмо на Яндекс Диск в Markdown;
|
|
189
|
+
- создать событие календаря из письма;
|
|
190
|
+
- добавить отправителя письма в контакты;
|
|
191
|
+
- найти в письме школу, детский сад или ИНН и подтянуть карточку из открытых городских слоев;
|
|
192
|
+
- найти адреса в письме и дать ссылки на Яндекс.Карты;
|
|
193
|
+
- создать локальную задачу по письму;
|
|
194
|
+
- отправить письмо по имени контакта из Яндекс Контактов;
|
|
195
|
+
- если найдено несколько однофамильцев, попросить уточнить получателя;
|
|
196
|
+
- если контакт найден, но email не указан, прямо сообщить об этом;
|
|
197
|
+
- добавить email в существующий контакт по явной просьбе пользователя.
|
|
184
198
|
|
|
185
199
|
Список писем и поиск не меняют статус письма. Статус меняется только при чтении письма, явной пометке или явном удалении.
|
|
186
200
|
|
|
201
|
+
Автоопрос почты использует локальный cron CLI. Он не передает письма на сервер IOLA: токены, состояние последнего UID и расписание хранятся на компьютере пользователя.
|
|
202
|
+
|
|
187
203
|
### yandex-calendar
|
|
188
204
|
|
|
189
205
|
Работать с Яндекс Календарем:
|