@iola_adm/iola-cli 0.2.39 → 0.2.40

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +151 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -3788,6 +3788,10 @@ async function buildYandexGoDeeplinkFromOptions(options = {}) {
3788
3788
  const from = options.from || options._?.[0] || "";
3789
3789
  const to = options.to || options._?.[1] || "";
3790
3790
  if (!from || !to) throw new Error('Укажите маршрут: iola yandex go link --from "Медведево, Школьная 15" --to "Медведево, Советская 20"');
3791
+ const ambiguous = [from, to].find((point) => isAmbiguousPersonalYandexGoPoint(point));
3792
+ if (ambiguous) {
3793
+ throw new Error(`Не знаю адрес "${ambiguous}". Укажите полный адрес отправления или сохраните его в настройках позже. Пример: "такси от Йошкар-Ола, улица ..., дом ... до Администрации Йошкар-Олы".`);
3794
+ }
3791
3795
  const fromPoint = await resolveYandexGoPoint(from);
3792
3796
  const toPoint = await resolveYandexGoPoint(to);
3793
3797
  const tariff = normalizeYandexGoTariff(options.tariff || options.class || options.level || "econom");
@@ -3810,10 +3814,46 @@ async function buildYandexGoDeeplinkFromOptions(options = {}) {
3810
3814
  async function resolveYandexGoPoint(query) {
3811
3815
  const parsed = parseLonLat(query);
3812
3816
  if (parsed) return { lon: parsed.lon, lat: parsed.lat, label: `${parsed.lat}, ${parsed.lon}`, address: "" };
3813
- const point = await callYandexGeocoder(query);
3817
+ const normalizedQuery = normalizeYandexGoGeocoderQuery(query);
3818
+ const point = await callYandexGeocoder(normalizedQuery);
3814
3819
  const coords = parseCoordinates(point?.coordinates);
3815
3820
  if (!Number.isFinite(coords.lat) || !Number.isFinite(coords.lon)) throw new Error(`Не смог получить координаты: ${query}`);
3816
- return { lon: coords.lon, lat: coords.lat, label: point.name || query, address: point.address || "" };
3821
+ if (!isExpectedYandexGoGeocoderResult(normalizedQuery, point)) {
3822
+ throw new Error(`Геокодер вернул неподходящий адрес для "${query}": ${point?.address || point?.name || "-"}. Уточните адрес полностью, например с городом и улицей.`);
3823
+ }
3824
+ return { lon: coords.lon, lat: coords.lat, label: point.name || query, address: point.address || normalizedQuery };
3825
+ }
3826
+
3827
+ function isAmbiguousPersonalYandexGoPoint(query) {
3828
+ const text = String(query || "").trim().toLocaleLowerCase("ru-RU");
3829
+ return /^(?:дом|дома|мой дом|у меня|у меня дома|от меня|здесь|тут|текущее место|моя геопозиция|мое местоположение)$/iu.test(text);
3830
+ }
3831
+
3832
+ function normalizeYandexGoGeocoderQuery(query) {
3833
+ const text = String(query || "").trim();
3834
+ const lower = text.toLocaleLowerCase("ru-RU");
3835
+ if (/администрац/iu.test(lower) && /(йошкар|иошкар|йошк|yoshkar|yoshkar-ola)/iu.test(lower)) {
3836
+ return "Россия, Республика Марий Эл, Йошкар-Ола, Ленинский проспект, 27";
3837
+ }
3838
+ if (/^(?:администрац(?:ия|ии)?|мэрия)$/iu.test(lower)) {
3839
+ return "Россия, Республика Марий Эл, Йошкар-Ола, Ленинский проспект, 27";
3840
+ }
3841
+ if (!/(йошкар|медведево|сем[её]новк|республика\s+марий\s+эл|марий\s+эл)/iu.test(lower)
3842
+ && /(администрац|мэрия|школ|сад|лицей|гимназ|ул\.|улица|проспект|пр-т|бульвар|переулок|дом|д\.|\d)/iu.test(lower)) {
3843
+ return `Россия, Республика Марий Эл, Йошкар-Ола, ${text}`;
3844
+ }
3845
+ return text;
3846
+ }
3847
+
3848
+ function isExpectedYandexGoGeocoderResult(query, point) {
3849
+ const expected = String(query || "").toLocaleLowerCase("ru-RU");
3850
+ const actual = `${point?.address || ""} ${point?.name || ""}`.toLocaleLowerCase("ru-RU");
3851
+ if (/(йошкар|ленинский проспект,\s*27|ленинский проспект,\s*дом\s*27)/iu.test(expected)) {
3852
+ return /йошкар-ола|йошкар ола|ленинский проспект,\s*27|ленинский проспект,\s*дом\s*27/iu.test(actual);
3853
+ }
3854
+ if (/медведево/iu.test(expected)) return /медведево/iu.test(actual);
3855
+ if (/сем[её]новк/iu.test(expected)) return /сем[её]новк/iu.test(actual);
3856
+ return true;
3817
3857
  }
3818
3858
 
3819
3859
  function parseLonLat(value) {
@@ -4909,7 +4949,7 @@ async function yandexMailList(options = {}) {
4909
4949
  const search = await imapCommand(session, `UID SEARCH ${criterion}`);
4910
4950
  const uids = parseImapSearchUids(search).slice(-Number(options.limit || 10));
4911
4951
  if (!uids.length) return [];
4912
- const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] BODY.PEEK[TEXT]<0.800>)`, { timeout: 45000 });
4952
+ const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] BODY.PEEK[TEXT]<0.3000>)`, { timeout: 45000 });
4913
4953
  return parseImapFetchSummaries(fetch).sort((left, right) => Number(right.uid || 0) - Number(left.uid || 0));
4914
4954
  } finally {
4915
4955
  await imapClose(session);
@@ -5638,7 +5678,7 @@ function parseImapFetchSummaries(text, options = {}) {
5638
5678
  const subject = headers.subject || "";
5639
5679
  const from = headers.from || "";
5640
5680
  const date = headers.date || "";
5641
- const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 800);
5681
+ const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 3000);
5642
5682
  rows.push({
5643
5683
  uid,
5644
5684
  date,
@@ -5769,12 +5809,55 @@ function decodeEmbeddedBase64MailBody(value) {
5769
5809
  for (const match of matches) {
5770
5810
  const clean = match.replace(/\s+/g, "");
5771
5811
  if (!/^[A-Z0-9+/]+={0,2}$/iu.test(clean) || clean.length < 160) continue;
5772
- const candidate = Buffer.from(clean, "base64").toString("utf8");
5812
+ const candidate = decodeBase64Utf8Candidate(clean);
5773
5813
  if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(candidate)) return candidate;
5774
5814
  }
5775
5815
  return text;
5776
5816
  }
5777
5817
 
5818
+ function decodeBase64Utf8Candidate(value) {
5819
+ const clean = String(value || "").replace(/\s+/g, "");
5820
+ const padded = clean + "=".repeat((4 - (clean.length % 4)) % 4);
5821
+ return Buffer.from(padded, "base64").toString("utf8");
5822
+ }
5823
+
5824
+ function looksLikeEncodedMailText(value) {
5825
+ const text = String(value || "").trim();
5826
+ if (!text) return false;
5827
+ const compact = text.replace(/\s+/g, "");
5828
+ if (compact.length < 80) return false;
5829
+ if (/^[A-Z0-9+/]+={0,2}$/iu.test(compact)) return true;
5830
+ const encodedChars = (text.match(/[A-Z0-9+/=]/giu) || []).length;
5831
+ return encodedChars / Math.max(text.length, 1) > 0.82 && !/[А-Яа-яЁё]{3,}/u.test(text);
5832
+ }
5833
+
5834
+ function cleanupDecodedMailText(value) {
5835
+ return String(value || "")
5836
+ .replace(/<style\b[\s\S]*?<\/style>/giu, " ")
5837
+ .replace(/<script\b[\s\S]*?<\/script>/giu, " ")
5838
+ .replace(/<[^>]+>/g, " ")
5839
+ .replace(/https?:\/\/\S+/giu, " ")
5840
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]+/gu, " ")
5841
+ .replace(/[^\S\n]+/g, " ")
5842
+ .replace(/\n{3,}/g, "\n\n")
5843
+ .trim();
5844
+ }
5845
+
5846
+ function normalizeMailSnippetForDisplay(value, maxLength = 600) {
5847
+ let text = String(value || "").trim();
5848
+ if (!text) return "";
5849
+ if (looksLikeEncodedMailText(text)) {
5850
+ const compact = text.replace(/\s+/g, "");
5851
+ const decoded = cleanupDecodedMailText(decodeBase64Utf8Candidate(compact));
5852
+ if (/[А-Яа-яЁё]{3,}/u.test(decoded) || /[A-ZА-Яа-яЁё]{3,}\s+[A-ZА-Яа-яЁё]{3,}/u.test(decoded)) {
5853
+ text = decoded;
5854
+ }
5855
+ }
5856
+ text = cleanupDecodedMailText(decodeEmbeddedBase64MailBody(text));
5857
+ if (looksLikeEncodedMailText(text)) return "";
5858
+ return text.replace(/\s+/g, " ").trim().slice(0, maxLength);
5859
+ }
5860
+
5778
5861
  async function yandexDavRequest(url, token, options = {}) {
5779
5862
  const response = await fetch(url, {
5780
5863
  method: options.method || "GET",
@@ -12678,6 +12761,26 @@ async function buildYandexDirectAnswer(question, history = []) {
12678
12761
  latest[0] ? `Самое свежее: ${formatYandexMailSummary(latest[0])}` : "",
12679
12762
  ].filter(Boolean).join("\n");
12680
12763
  }
12764
+ const recentHours = extractRecentMailHours(question);
12765
+ if (recentHours) {
12766
+ const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
12767
+ const rows = await yandexMailList({ mailbox, limit: 50, unread: /непрочитан/iu.test(normalized) });
12768
+ const since = Date.now() - recentHours * 60 * 60 * 1000;
12769
+ const recentRows = rows.filter((row) => {
12770
+ const time = Date.parse(row.date || "");
12771
+ return Number.isFinite(time) && time >= since;
12772
+ });
12773
+ if (!recentRows.length) return `За последние ${recentHours} ${formatRussianHours(recentHours)} писем не найдено.`;
12774
+ const detailedRows = [];
12775
+ for (const row of recentRows.slice(0, 10)) {
12776
+ const detailed = await yandexMailRead(row.uid, { mailbox, markSeen: false }).catch(() => null);
12777
+ detailedRows.push(detailed && detailed.status !== "not-found" ? { ...row, ...detailed } : row);
12778
+ }
12779
+ return [
12780
+ `За последние ${recentHours} ${formatRussianHours(recentHours)} нашел ${recentRows.length} ${formatRussianLetters(recentRows.length)}:`,
12781
+ ...detailedRows.map((row, index) => formatYandexMailDigestItem(row, index + 1)),
12782
+ ].join("\n");
12783
+ }
12681
12784
  if (isYandexMailReadRequest(normalized)) {
12682
12785
  const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText)
12683
12786
  || await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
@@ -12686,6 +12789,7 @@ async function buildYandexDirectAnswer(question, history = []) {
12686
12789
  if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
12687
12790
  return formatYandexMailRead(row);
12688
12791
  }
12792
+
12689
12793
  const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
12690
12794
  const rows = /(найди|поиск)/iu.test(normalized)
12691
12795
  ? await yandexMailSearch(cleanupYandexQuery(question), { mailbox, limit: 10 })
@@ -12892,7 +12996,7 @@ async function buildYandexDirectAnswer(question, history = []) {
12892
12996
  }
12893
12997
  } catch (error) {
12894
12998
  const message = error instanceof Error ? error.message : String(error);
12895
- if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT нужны)/u.test(message)) return message;
12999
+ if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT нужны|Не знаю адрес|Геокодер вернул неподходящий адрес)/u.test(message)) return message;
12896
13000
  return `Не смог выполнить запрос к сервисам Яндекса: ${message}`;
12897
13001
  }
12898
13002
  return "";
@@ -13327,8 +13431,19 @@ function formatYandexMailSummary(row) {
13327
13431
  return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
13328
13432
  }
13329
13433
 
13434
+ function formatYandexMailDigestItem(row, index) {
13435
+ const snippet = normalizeMailSnippetForDisplay(row.snippet, 350);
13436
+ return [
13437
+ `${index}. Письмо #${row.uid}`,
13438
+ ` От: ${row.from || "-"}`,
13439
+ ` Тема: ${row.subject || "(без темы)"}`,
13440
+ row.date ? ` Дата: ${row.date}` : "",
13441
+ snippet ? ` О чем: ${snippet}` : " О чем: текст письма не распознан, доступна только тема.",
13442
+ ].filter(Boolean).join("\n");
13443
+ }
13444
+
13330
13445
  function formatYandexMailRead(row) {
13331
- const body = String(row.snippet || "").trim();
13446
+ const body = normalizeMailSnippetForDisplay(row.snippet, 2000);
13332
13447
  return [
13333
13448
  `Письмо #${row.uid}`,
13334
13449
  `От: ${row.from || "-"}`,
@@ -13339,6 +13454,35 @@ function formatYandexMailRead(row) {
13339
13454
  ].filter((line) => line !== "").join("\n");
13340
13455
  }
13341
13456
 
13457
+ function extractRecentMailHours(question) {
13458
+ const text = String(question || "").toLocaleLowerCase("ru-RU");
13459
+ if (!/(последн|прошедш|за\s+\d+|за\s+два|за\s+три|за\s+час)/iu.test(text)) return 0;
13460
+ if (!/(час|минут|сут|день|дня|дней)/iu.test(text)) return 0;
13461
+ const numeric = Number(text.match(/(\d+)\s*(?:час|часа|часов)/iu)?.[1] || 0);
13462
+ if (numeric > 0) return numeric;
13463
+ const words = { один: 1, одну: 1, два: 2, две: 2, три: 3, четыре: 4, пять: 5, шесть: 6, семь: 7, восемь: 8, девять: 9, десять: 10 };
13464
+ const word = text.match(/(?:^|\s)(один|одну|два|две|три|четыре|пять|шесть|семь|восемь|девять|десять)\s*(?:час|часа|часов)(?:\s|$|[,.!?;:])/iu)?.[1];
13465
+ if (word && words[word]) return words[word];
13466
+ const minutes = Number(text.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 0);
13467
+ if (minutes > 0) return Math.max(1, Math.ceil(minutes / 60));
13468
+ if (/сутк|день|дня|дней/u.test(text)) return 24;
13469
+ return /час/u.test(text) ? 1 : 0;
13470
+ }
13471
+
13472
+ function formatRussianHours(value) {
13473
+ const n = Math.abs(Number(value || 0));
13474
+ if (n % 10 === 1 && n % 100 !== 11) return "час";
13475
+ if ([2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100)) return "часа";
13476
+ return "часов";
13477
+ }
13478
+
13479
+ function formatRussianLetters(value) {
13480
+ const n = Math.abs(Number(value || 0));
13481
+ if (n % 10 === 1 && n % 100 !== 11) return "письмо";
13482
+ if ([2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100)) return "письма";
13483
+ return "писем";
13484
+ }
13485
+
13342
13486
  function slugForFile(value) {
13343
13487
  return String(value || "")
13344
13488
  .toLocaleLowerCase("ru-RU")