@iola_adm/iola-cli 0.2.30 → 0.2.31
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
|
@@ -29,12 +29,14 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
29
29
|
- `yandex_mail_reply` - ответить на письмо по UID.
|
|
30
30
|
- `yandex_mail_delete` - переместить письмо в корзину.
|
|
31
31
|
- `yandex_mail_mark` - пометить письмо прочитанным или непрочитанным.
|
|
32
|
+
- Автоопрос почты включается командой `iola yandex mail-watch on --minutes 5`, выключается `iola yandex mail-watch off`, ручная проверка `iola yandex mail-watch tick`.
|
|
32
33
|
- `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
|
|
33
34
|
- `yandex_calendar_list` - показать события.
|
|
34
35
|
- `yandex_calendar_create_event` - создать событие.
|
|
35
36
|
- `yandex_contacts_status` - проверить доступ к Яндекс Контактам.
|
|
36
37
|
- `yandex_contacts_list` - показать контакты.
|
|
37
38
|
- `yandex_contacts_search` - найти контакт.
|
|
39
|
+
- `yandex_contacts_add_email` - добавить email в существующий контакт.
|
|
38
40
|
- `yandex_telemost_create_event` - создать календарное событие для встречи.
|
|
39
41
|
|
|
40
42
|
Безопасность:
|
|
@@ -42,5 +44,7 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
42
44
|
- Для отправки письма, ответа на письмо, удаления письма, удаления файлов, публикации ссылки и создания событий нужен явный запрос пользователя и `confirm=true`.
|
|
43
45
|
- Не отправляй письма, не отвечай на письма, не удаляй письма/файлы и не публикуй ссылки по косвенному намерению.
|
|
44
46
|
- Список писем и поиск не должны помечать письма прочитанными. Чтение письма по просьбе пользователя помечает письмо прочитанным.
|
|
47
|
+
- Если пользователь просит отправить письмо по имени контакта, сначала ищи контакт. Если контактов несколько, покажи варианты и попроси уточнить. Если контакта нет или у него нет email, скажи прямо.
|
|
48
|
+
- Если пользователь отправил письмо на введенный email и указал имя контакта, можно предложить добавить email в найденный контакт без email.
|
|
45
49
|
- Не выводи OAuth-токены, API-ключи, пароли и секреты.
|
|
46
50
|
- Если сервис не подключен, скажи запустить `iola yandex setup` или открыть `/master`.
|
package/src/cli.js
CHANGED
|
@@ -178,6 +178,7 @@ const YANDEX_TOOLS = [
|
|
|
178
178
|
"yandex_contacts_status",
|
|
179
179
|
"yandex_contacts_list",
|
|
180
180
|
"yandex_contacts_search",
|
|
181
|
+
"yandex_contacts_add_email",
|
|
181
182
|
"yandex_telemost_create_event",
|
|
182
183
|
];
|
|
183
184
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS];
|
|
@@ -3308,6 +3309,11 @@ async function handleYandex(args) {
|
|
|
3308
3309
|
return;
|
|
3309
3310
|
}
|
|
3310
3311
|
|
|
3312
|
+
if (action === "mail-watch" || action === "mailwatch" || action === "watch-mail") {
|
|
3313
|
+
await handleYandexMailWatch([target, ...rest].filter(Boolean));
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3311
3317
|
if (action === "enable" || action === "disable") {
|
|
3312
3318
|
const services = [target, ...rest].filter((item) => item && !String(item).startsWith("--"));
|
|
3313
3319
|
if (services.length === 0) throw new Error("Укажите сервисы. Пример: iola yandex enable disk mail calendar");
|
|
@@ -3343,6 +3349,7 @@ async function handleYandex(args) {
|
|
|
3343
3349
|
iola yandex menu
|
|
3344
3350
|
iola yandex status|doctor
|
|
3345
3351
|
iola yandex services
|
|
3352
|
+
iola yandex mail-watch on|off|status|tick [--minutes 5]
|
|
3346
3353
|
iola yandex enable disk mail calendar
|
|
3347
3354
|
iola yandex disable mail
|
|
3348
3355
|
iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
|
|
@@ -3372,6 +3379,47 @@ function printYandexServices(options = {}) {
|
|
|
3372
3379
|
]);
|
|
3373
3380
|
}
|
|
3374
3381
|
|
|
3382
|
+
async function handleYandexMailWatch(args = []) {
|
|
3383
|
+
const [action = "status", ...rest] = args;
|
|
3384
|
+
const options = parseOptions(rest);
|
|
3385
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3386
|
+
const minutes = Math.max(1, Number(options.minutes || options.interval || rest.find((item) => /^\d+$/u.test(String(item))) || 5));
|
|
3387
|
+
const result = await yandexMailWatchEnable(minutes);
|
|
3388
|
+
console.log(`Автопроверка новых писем включена: каждые ${minutes} минут. Текущий последний UID: ${result.lastUid || "-"}.`);
|
|
3389
|
+
console.log("Для работы по расписанию должен запускаться cron tick: вручную, через daemon или Windows Task Scheduler.");
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3393
|
+
await yandexMailWatchDisable();
|
|
3394
|
+
console.log("Автопроверка новых писем выключена.");
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3398
|
+
const result = await yandexMailWatchTick(options);
|
|
3399
|
+
if (!result.enabled) {
|
|
3400
|
+
console.log("Автопроверка новых писем выключена.");
|
|
3401
|
+
} else if (!result.newMessages.length) {
|
|
3402
|
+
console.log("Новых писем нет.");
|
|
3403
|
+
} else {
|
|
3404
|
+
console.log(["Новые письма:", ...result.newMessages.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n"));
|
|
3405
|
+
}
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
if (action === "status" || action === "doctor") {
|
|
3409
|
+
const config = await loadConfig();
|
|
3410
|
+
const watch = config.yandex?.mailWatch || {};
|
|
3411
|
+
printKeyValue({
|
|
3412
|
+
enabled: watch.enabled ? "yes" : "no",
|
|
3413
|
+
minutes: watch.minutes || "-",
|
|
3414
|
+
mailbox: watch.mailbox || "INBOX",
|
|
3415
|
+
lastUid: watch.lastUid || "-",
|
|
3416
|
+
cron: listCronJobs().some((job) => job.command === "yandex mail-watch tick") ? "yes" : "no",
|
|
3417
|
+
});
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3375
3423
|
async function setupYandexConnector(args = []) {
|
|
3376
3424
|
const options = parseOptions(args);
|
|
3377
3425
|
const config = await loadConfig();
|
|
@@ -3967,6 +4015,7 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
3967
4015
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
3968
4016
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
3969
4017
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
4018
|
+
if (tool === "yandex_contacts_add_email") return yandexContactsAddEmail(args.query || args.name || "", args.email, args);
|
|
3970
4019
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
3971
4020
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
3972
4021
|
}
|
|
@@ -4087,6 +4136,52 @@ async function yandexMailSearch(query, options = {}) {
|
|
|
4087
4136
|
return rows.filter((row) => normalizeGeoText(`${row.from} ${row.subject} ${row.snippet}`).includes(normalized)).slice(0, Number(options.limit || 20));
|
|
4088
4137
|
}
|
|
4089
4138
|
|
|
4139
|
+
async function yandexMailWatchTick(options = {}) {
|
|
4140
|
+
const config = await loadConfig();
|
|
4141
|
+
const watch = config.yandex?.mailWatch || {};
|
|
4142
|
+
if (!watch.enabled && !options.force) return { enabled: false, newMessages: [] };
|
|
4143
|
+
const mailbox = watch.mailbox || "INBOX";
|
|
4144
|
+
const lastUid = Number(watch.lastUid || 0);
|
|
4145
|
+
const rows = await yandexMailList({ mailbox, limit: Number(options.limit || 30), unread: false });
|
|
4146
|
+
const newMessages = rows.filter((row) => Number(row.uid || 0) > lastUid).sort((left, right) => Number(left.uid || 0) - Number(right.uid || 0));
|
|
4147
|
+
const newestUid = Math.max(lastUid, ...rows.map((row) => Number(row.uid || 0)));
|
|
4148
|
+
await saveConfig({
|
|
4149
|
+
yandex: {
|
|
4150
|
+
...(config.yandex || {}),
|
|
4151
|
+
mailWatch: {
|
|
4152
|
+
...watch,
|
|
4153
|
+
enabled: watch.enabled !== false,
|
|
4154
|
+
mailbox,
|
|
4155
|
+
lastUid: newestUid,
|
|
4156
|
+
lastCheckedAt: new Date().toISOString(),
|
|
4157
|
+
},
|
|
4158
|
+
},
|
|
4159
|
+
});
|
|
4160
|
+
return { enabled: true, mailbox, lastUid, newestUid, newMessages };
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
async function yandexMailWatchEnable(minutes = 5) {
|
|
4164
|
+
const config = await loadConfig();
|
|
4165
|
+
const safeMinutes = Math.max(1, Number(minutes || 5));
|
|
4166
|
+
const latest = await yandexMailList({ mailbox: "INBOX", limit: 1, unread: false });
|
|
4167
|
+
const lastUid = latest[0]?.uid || 0;
|
|
4168
|
+
await saveConfig({
|
|
4169
|
+
yandex: {
|
|
4170
|
+
...(config.yandex || {}),
|
|
4171
|
+
mailWatch: { enabled: true, minutes: safeMinutes, mailbox: "INBOX", lastUid, updatedAt: new Date().toISOString() },
|
|
4172
|
+
},
|
|
4173
|
+
});
|
|
4174
|
+
await upsertCronJob(`каждые ${safeMinutes} минут`, "yandex mail-watch tick", { replaceCommand: true });
|
|
4175
|
+
return { enabled: true, minutes: safeMinutes, mailbox: "INBOX", lastUid };
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
async function yandexMailWatchDisable() {
|
|
4179
|
+
const config = await loadConfig();
|
|
4180
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), mailWatch: { ...(config.yandex?.mailWatch || {}), enabled: false, updatedAt: new Date().toISOString() } } });
|
|
4181
|
+
deleteCronJobsByCommand("yandex mail-watch tick");
|
|
4182
|
+
return { enabled: false };
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4090
4185
|
async function yandexMailRead(uid, options = {}) {
|
|
4091
4186
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
4092
4187
|
const { token, email } = await yandexMailCredentials();
|
|
@@ -4573,7 +4668,7 @@ async function yandexContactsList(args = {}) {
|
|
|
4573
4668
|
const rows = [];
|
|
4574
4669
|
for (const href of hrefs) {
|
|
4575
4670
|
const card = await yandexDavRequest(new URL(href, "https://carddav.yandex.ru/").toString(), token, { method: "GET", timeout: 30000 }).catch(() => "");
|
|
4576
|
-
rows.push(...parseVCards(card));
|
|
4671
|
+
rows.push(...parseVCards(card).map((row) => args.full ? { ...row, href, card } : row));
|
|
4577
4672
|
}
|
|
4578
4673
|
return rows.slice(0, Number(args.limit || 50));
|
|
4579
4674
|
}
|
|
@@ -4585,6 +4680,64 @@ async function yandexContactsSearch(query, args = {}) {
|
|
|
4585
4680
|
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.email} ${row.phone}`).includes(normalized)).slice(0, Number(args.limit || 20));
|
|
4586
4681
|
}
|
|
4587
4682
|
|
|
4683
|
+
async function resolveYandexMailRecipientFromContacts(query) {
|
|
4684
|
+
const normalized = normalizeContactLookupText(query);
|
|
4685
|
+
if (!normalized) return { status: "not-found", contacts: [] };
|
|
4686
|
+
const rows = await yandexContactsList({ limit: 300 });
|
|
4687
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, 10);
|
|
4688
|
+
if (!matches.length) return { status: "not-found", contacts: [] };
|
|
4689
|
+
const withEmail = matches.filter((row) => row.email);
|
|
4690
|
+
if (withEmail.length === 1) return { status: "ok", contact: withEmail[0] };
|
|
4691
|
+
if (withEmail.length > 1) return { status: "ambiguous", contacts: withEmail };
|
|
4692
|
+
return matches.length === 1
|
|
4693
|
+
? { status: "no-email", contact: matches[0] }
|
|
4694
|
+
: { status: "ambiguous", contacts: matches };
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
async function yandexContactsAddEmail(query, email, args = {}) {
|
|
4698
|
+
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
4699
|
+
if (!query) throw new Error("Укажите имя или часть имени контакта.");
|
|
4700
|
+
if (!email || !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(String(email))) throw new Error("Укажите корректный email.");
|
|
4701
|
+
const rows = await yandexContactsList({ limit: 500, full: true });
|
|
4702
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalizeContactLookupText(query))).slice(0, 10);
|
|
4703
|
+
if (!matches.length) return { status: "not-found", query };
|
|
4704
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", query, contacts: matches.map(({ card, ...row }) => row) };
|
|
4705
|
+
const contact = matches[0];
|
|
4706
|
+
if (contact.email && !args.overwrite) return { status: "has-email", contact: { name: contact.name, email: contact.email } };
|
|
4707
|
+
const updatedCard = upsertVcardEmail(contact.card, email, { overwrite: Boolean(args.overwrite) });
|
|
4708
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4709
|
+
await yandexDavRequest(new URL(contact.href, "https://carddav.yandex.ru/").toString(), token, {
|
|
4710
|
+
method: "PUT",
|
|
4711
|
+
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4712
|
+
body: updatedCard,
|
|
4713
|
+
timeout: 45000,
|
|
4714
|
+
});
|
|
4715
|
+
return { status: "updated", name: contact.name, email };
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
function upsertVcardEmail(card, email, options = {}) {
|
|
4719
|
+
const text = String(card || "").replace(/\r/g, "").trim();
|
|
4720
|
+
if (!text.includes("BEGIN:VCARD")) throw new Error("Контакт не похож на vCard.");
|
|
4721
|
+
if (/^EMAIL[^:]*:/imu.test(text)) {
|
|
4722
|
+
if (!options.overwrite) return text;
|
|
4723
|
+
return text.replace(/^EMAIL[^:]*:[^\n]*/imu, `EMAIL;TYPE=INTERNET:${email}`);
|
|
4724
|
+
}
|
|
4725
|
+
return text.replace(/\nEND:VCARD/iu, `\nEMAIL;TYPE=INTERNET:${email}\nEND:VCARD`);
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
function contactMatchesQuery(contact, normalizedQuery) {
|
|
4729
|
+
const contactText = normalizeContactLookupText(`${contact.name || ""} ${contact.email || ""}`);
|
|
4730
|
+
if (!contactText || !normalizedQuery) return false;
|
|
4731
|
+
if (contactText.includes(normalizedQuery)) return true;
|
|
4732
|
+
const queryTokens = normalizedQuery.split(/\s+/u).filter(Boolean);
|
|
4733
|
+
const contactTokens = contactText.split(/\s+/u).filter(Boolean);
|
|
4734
|
+
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))))));
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
function normalizeContactLookupText(value) {
|
|
4738
|
+
return normalizeGeoText(String(value || "").replace(/\b(?:кому|контакт|письмо|сообщение)\b/giu, " ")).trim();
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4588
4741
|
async function yandexContactsBaseUrl(token) {
|
|
4589
4742
|
const root = "https://carddav.yandex.ru/";
|
|
4590
4743
|
const principalXml = await yandexDavRequest(root, token, {
|
|
@@ -9273,6 +9426,25 @@ function addCronJob(scheduleText, command) {
|
|
|
9273
9426
|
}
|
|
9274
9427
|
}
|
|
9275
9428
|
|
|
9429
|
+
async function upsertCronJob(scheduleText, command, options = {}) {
|
|
9430
|
+
initDatabase();
|
|
9431
|
+
const db = openDatabase();
|
|
9432
|
+
try {
|
|
9433
|
+
const existing = db.prepare("SELECT id FROM cron_jobs WHERE command = ? ORDER BY id DESC LIMIT 1").get(command);
|
|
9434
|
+
if (existing?.id) {
|
|
9435
|
+
db.prepare("UPDATE cron_jobs SET schedule_text = ?, enabled = 1 WHERE id = ?").run(scheduleText, existing.id);
|
|
9436
|
+
return Number(existing.id);
|
|
9437
|
+
}
|
|
9438
|
+
if (options.replaceCommand) {
|
|
9439
|
+
db.prepare("DELETE FROM cron_jobs WHERE command = ?").run(command);
|
|
9440
|
+
}
|
|
9441
|
+
const result = db.prepare("INSERT INTO cron_jobs(schedule_text, command) VALUES (?, ?)").run(scheduleText, command);
|
|
9442
|
+
return Number(result.lastInsertRowid);
|
|
9443
|
+
} finally {
|
|
9444
|
+
db.close();
|
|
9445
|
+
}
|
|
9446
|
+
}
|
|
9447
|
+
|
|
9276
9448
|
function deleteCronJob(id) {
|
|
9277
9449
|
initDatabase();
|
|
9278
9450
|
const db = openDatabase();
|
|
@@ -9283,6 +9455,16 @@ function deleteCronJob(id) {
|
|
|
9283
9455
|
}
|
|
9284
9456
|
}
|
|
9285
9457
|
|
|
9458
|
+
function deleteCronJobsByCommand(command) {
|
|
9459
|
+
initDatabase();
|
|
9460
|
+
const db = openDatabase();
|
|
9461
|
+
try {
|
|
9462
|
+
db.prepare("DELETE FROM cron_jobs WHERE command = ?").run(command);
|
|
9463
|
+
} finally {
|
|
9464
|
+
db.close();
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
|
|
9286
9468
|
function dueCronJobs() {
|
|
9287
9469
|
initDatabase();
|
|
9288
9470
|
const db = openDatabase();
|
|
@@ -9829,6 +10011,20 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9829
10011
|
}
|
|
9830
10012
|
|
|
9831
10013
|
if (mailFollowup || /(почт|письм|email|e-mail|спам|чернов|отправлен|исходящ|корзин)/iu.test(normalized)) {
|
|
10014
|
+
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
10015
|
+
const minutes = Number(String(question || "").match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 5);
|
|
10016
|
+
await yandexMailWatchEnable(minutes);
|
|
10017
|
+
return `Автопроверка новых писем включена: каждые ${minutes} минут.`;
|
|
10018
|
+
}
|
|
10019
|
+
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
10020
|
+
await yandexMailWatchDisable();
|
|
10021
|
+
return "Автопроверка новых писем выключена.";
|
|
10022
|
+
}
|
|
10023
|
+
if (/(проверь|получи|есть).{0,40}(нов|свеж)/iu.test(normalized)) {
|
|
10024
|
+
const result = await yandexMailWatchTick({ force: true });
|
|
10025
|
+
if (!result.newMessages.length) return "Новых писем нет.";
|
|
10026
|
+
return ["Новые письма:", ...result.newMessages.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
10027
|
+
}
|
|
9832
10028
|
if (/(папк|ящик|mailbox|folder)/iu.test(normalized) && /(покажи|список|какие|есть)/iu.test(normalized)) {
|
|
9833
10029
|
const folders = await yandexMailFolders();
|
|
9834
10030
|
if (!folders.length) return "Папки Яндекс Почты не найдены.";
|
|
@@ -9861,11 +10057,34 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9861
10057
|
}
|
|
9862
10058
|
if (/(отправь|отправить|напиши|пошли)/iu.test(normalized) && !/(отправлен|исходящ)/iu.test(normalized)) {
|
|
9863
10059
|
const draft = parseYandexMailSendRequest(question);
|
|
10060
|
+
if (!draft.to.length && draft.contactQuery) {
|
|
10061
|
+
const contactResult = await resolveYandexMailRecipientFromContacts(draft.contactQuery);
|
|
10062
|
+
if (contactResult.status === "not-found") return `В Яндекс Контактах не нашел: ${draft.contactQuery}. Укажите email вручную.`;
|
|
10063
|
+
if (contactResult.status === "no-email") return `Контакт найден, но email не указан: ${contactResult.contact.name}. Укажите email вручную.`;
|
|
10064
|
+
if (contactResult.status === "ambiguous") {
|
|
10065
|
+
return [
|
|
10066
|
+
`Нашел несколько контактов для "${draft.contactQuery}". Уточните, кому отправить:`,
|
|
10067
|
+
...contactResult.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ", email не указан"}`),
|
|
10068
|
+
].join("\n");
|
|
10069
|
+
}
|
|
10070
|
+
draft.to = [contactResult.contact.email];
|
|
10071
|
+
}
|
|
9864
10072
|
if (!draft.to.length || !draft.text) {
|
|
9865
10073
|
return "Для отправки письма укажите получателя и текст. Пример: отправь письмо user@example.com тема: Привет текст: Проверка.";
|
|
9866
10074
|
}
|
|
9867
10075
|
const result = await yandexMailSend({ ...draft, confirm: true });
|
|
9868
|
-
|
|
10076
|
+
let contactNote = "";
|
|
10077
|
+
if (draft.contactQuery && result.to[0] && process.stdin.isTTY) {
|
|
10078
|
+
const contactResult = await resolveYandexMailRecipientFromContacts(draft.contactQuery).catch(() => null);
|
|
10079
|
+
if (contactResult?.status === "no-email") {
|
|
10080
|
+
const shouldAdd = await askYesNo(`Добавить ${result.to[0]} в контакт "${contactResult.contact.name}"? [y/N] `, false);
|
|
10081
|
+
if (shouldAdd) {
|
|
10082
|
+
const update = await yandexContactsAddEmail(draft.contactQuery, result.to[0], { confirm: true, selectFirst: true });
|
|
10083
|
+
contactNote = update.status === "updated" ? `\nEmail добавлен в контакт: ${update.name || draft.contactQuery}.` : "";
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
}
|
|
10087
|
+
return `Письмо отправлено: ${result.to.join(", ")}. Тема: ${result.subject}.${contactNote}`;
|
|
9869
10088
|
}
|
|
9870
10089
|
if (/(сколько|количеств|есть\s+ли)/iu.test(normalized) && /непрочитан/iu.test(normalized)) {
|
|
9871
10090
|
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
@@ -9900,6 +10119,21 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9900
10119
|
}
|
|
9901
10120
|
|
|
9902
10121
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
10122
|
+
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(normalized)) {
|
|
10123
|
+
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
10124
|
+
const query = cleanupYandexContactEmailTarget(question, email);
|
|
10125
|
+
if (!email || !query) return "Укажите контакт и email. Пример: добавь email petrov@example.com к контакту Петров.";
|
|
10126
|
+
const result = await yandexContactsAddEmail(query, email, { confirm: true });
|
|
10127
|
+
if (result.status === "not-found") return `Контакт не найден: ${query}.`;
|
|
10128
|
+
if (result.status === "has-email") return `У контакта уже есть email: ${result.contact.name}, ${result.contact.email}.`;
|
|
10129
|
+
if (result.status === "ambiguous") {
|
|
10130
|
+
return [
|
|
10131
|
+
`Нашел несколько контактов для "${query}". Уточните контакт:`,
|
|
10132
|
+
...result.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ""}`),
|
|
10133
|
+
].join("\n");
|
|
10134
|
+
}
|
|
10135
|
+
return `Email добавлен в контакт: ${result.name || query}, ${result.email}.`;
|
|
10136
|
+
}
|
|
9903
10137
|
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
9904
10138
|
const result = await yandexContactsStatus();
|
|
9905
10139
|
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
@@ -10024,6 +10258,7 @@ function parseYandexMailSendRequest(question) {
|
|
|
10024
10258
|
const emails = [...text.matchAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu)].map((match) => match[0]);
|
|
10025
10259
|
const subjectMatch = text.match(/(?:тема|subject)\s*:\s*(.*?)(?=\s+(?:текст|body|сообщение)\s*:|$)/iu);
|
|
10026
10260
|
const bodyMatch = text.match(/(?:текст|body|сообщение)\s*:\s*(.*)$/iu);
|
|
10261
|
+
const contactMatch = text.match(/^(?:отправь|отправить|напиши|пошли)\s+(.+?)(?:\s+письмо|\s+сообщение|\s+текст\s*:|\s+напомни|\s+скажи|$)/iu);
|
|
10027
10262
|
const withoutCommand = text
|
|
10028
10263
|
.replace(/^(?:отправь|отправить|напиши|пошли)\s+(?:письмо\s+)?/iu, "")
|
|
10029
10264
|
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "")
|
|
@@ -10031,10 +10266,33 @@ function parseYandexMailSendRequest(question) {
|
|
|
10031
10266
|
return {
|
|
10032
10267
|
to: emails,
|
|
10033
10268
|
subject: (subjectMatch?.[1] || "Сообщение от IOLA CLI").trim(),
|
|
10034
|
-
text: (bodyMatch?.[1] || (!subjectMatch ? withoutCommand : "")).trim(),
|
|
10269
|
+
text: (bodyMatch?.[1] || cleanupYandexMailBodyText(!subjectMatch ? withoutCommand : "")).trim(),
|
|
10270
|
+
contactQuery: cleanupYandexContactQuery((contactMatch?.[1] || "").replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "")),
|
|
10035
10271
|
};
|
|
10036
10272
|
}
|
|
10037
10273
|
|
|
10274
|
+
function cleanupYandexMailBodyText(value) {
|
|
10275
|
+
return String(value || "")
|
|
10276
|
+
.replace(/^.+?\s+(?:письмо|сообщение)\s+/iu, "")
|
|
10277
|
+
.trim();
|
|
10278
|
+
}
|
|
10279
|
+
|
|
10280
|
+
function cleanupYandexContactQuery(value) {
|
|
10281
|
+
return String(value || "")
|
|
10282
|
+
.replace(/\b(?:письмо|сообщение|текст|напомни|скажи|ему|ей)\b/giu, " ")
|
|
10283
|
+
.replace(/\s+/g, " ")
|
|
10284
|
+
.trim();
|
|
10285
|
+
}
|
|
10286
|
+
|
|
10287
|
+
function cleanupYandexContactEmailTarget(question, email) {
|
|
10288
|
+
return String(question || "")
|
|
10289
|
+
.replace(email || "", " ")
|
|
10290
|
+
.replace(/\b(?:добавь|добавить|запиши|сохрани|email|e-mail|почту|почта|к|ко|контакту|контакт)\b/giu, " ")
|
|
10291
|
+
.replace(/[,:;.!?]+/gu, " ")
|
|
10292
|
+
.replace(/\s+/g, " ")
|
|
10293
|
+
.trim();
|
|
10294
|
+
}
|
|
10295
|
+
|
|
10038
10296
|
function formatYandexMailSummary(row) {
|
|
10039
10297
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
10040
10298
|
}
|
package/test/smoke-test.js
CHANGED
|
@@ -89,6 +89,11 @@ 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");
|
|
92
97
|
assertIncludes(cliSource, "buildCasualDirectAnswer(question)", "Casual greetings should bypass external AI providers");
|
|
93
98
|
assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
|
|
94
99
|
if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
|
|
@@ -181,9 +181,18 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
181
181
|
- отправить письмо только после явного запроса;
|
|
182
182
|
- ответить на письмо по UID или номеру из последнего списка;
|
|
183
183
|
- удалить письмо безопасно: переместить его в корзину, а не стирать безвозвратно.
|
|
184
|
+
- включить автоопрос новых писем, например каждые 5 минут;
|
|
185
|
+
- выключить автоопрос новых писем;
|
|
186
|
+
- вручную проверить новые письма через mail-watch tick;
|
|
187
|
+
- отправить письмо по имени контакта из Яндекс Контактов;
|
|
188
|
+
- если найдено несколько однофамильцев, попросить уточнить получателя;
|
|
189
|
+
- если контакт найден, но email не указан, прямо сообщить об этом;
|
|
190
|
+
- добавить email в существующий контакт по явной просьбе пользователя.
|
|
184
191
|
|
|
185
192
|
Список писем и поиск не меняют статус письма. Статус меняется только при чтении письма, явной пометке или явном удалении.
|
|
186
193
|
|
|
194
|
+
Автоопрос почты использует локальный cron CLI. Он не передает письма на сервер IOLA: токены, состояние последнего UID и расписание хранятся на компьютере пользователя.
|
|
195
|
+
|
|
187
196
|
### yandex-calendar
|
|
188
197
|
|
|
189
198
|
Работать с Яндекс Календарем:
|