@iola_adm/iola-cli 0.2.39 → 0.2.41
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 +1 -1
- package/src/cli.js +164 -12
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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) {
|
|
@@ -3882,11 +3922,13 @@ function extractYandexGoRouteFromText(text) {
|
|
|
3882
3922
|
const source = String(text || "").trim();
|
|
3883
3923
|
const tariffMatch = source.match(/(эконом|комфорт\+?|комфорт плюс|бизнес|минивен|детск\w*|econom|business|comfortplus|minivan|vip)/iu);
|
|
3884
3924
|
const cleaned = source
|
|
3925
|
+
.replace(/(?:^|\s)(?:так|мне\s+нужно|мне\s+надо|нужно|надо)(?=\s|$)/giu, " ")
|
|
3885
3926
|
.replace(/^(?:построй|создай|дай|открой|сделай|подготовь|вызови|закажи)\s+/iu, "")
|
|
3886
|
-
.replace(
|
|
3927
|
+
.replace(/(?:^|\s)(?:яндекс\s*go|яндекс\s*го|такси|маршрут|ссылк[ау]?|диплинк|deeplink)(?=\s|$)/giu, " ")
|
|
3887
3928
|
.replace(/\s+/g, " ")
|
|
3888
3929
|
.trim();
|
|
3889
|
-
const match = cleaned.match(/(?:от|из|с)\s+(.+?)\s+(?:до|в|на)\s+(.+)$/iu)
|
|
3930
|
+
const match = cleaned.match(/(?:от|из|с)\s+(.+?)\s+(?:до|в|на)\s+(.+)$/iu)
|
|
3931
|
+
|| cleaned.match(/^(.+?)\s*,?\s+(?:до|в)\s+(.+)$/iu);
|
|
3890
3932
|
if (!match) return { from: "", to: "", tariff: tariffMatch ? normalizeYandexGoTariff(tariffMatch[1]) : "econom" };
|
|
3891
3933
|
const from = match[1].replace(/[,.;]\s*$/u, "").trim();
|
|
3892
3934
|
const to = match[2].replace(/[,.;]\s*(?:тариф|эконом|комфорт\+?|комфорт плюс|бизнес|минивен|детск\w*).*$/iu, "").trim();
|
|
@@ -4909,7 +4951,7 @@ async function yandexMailList(options = {}) {
|
|
|
4909
4951
|
const search = await imapCommand(session, `UID SEARCH ${criterion}`);
|
|
4910
4952
|
const uids = parseImapSearchUids(search).slice(-Number(options.limit || 10));
|
|
4911
4953
|
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.
|
|
4954
|
+
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
4955
|
return parseImapFetchSummaries(fetch).sort((left, right) => Number(right.uid || 0) - Number(left.uid || 0));
|
|
4914
4956
|
} finally {
|
|
4915
4957
|
await imapClose(session);
|
|
@@ -5257,7 +5299,8 @@ async function yandexMailReply(args = {}) {
|
|
|
5257
5299
|
if (!original || original.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
5258
5300
|
const to = [extractEmailAddress(original.from)].filter(Boolean);
|
|
5259
5301
|
if (!to.length) throw new Error("Не удалось определить получателя ответа из поля From.");
|
|
5260
|
-
const
|
|
5302
|
+
const requestedSubject = String(args.subject || "").trim();
|
|
5303
|
+
const subject = requestedSubject || (/^re:/iu.test(original.subject || "") ? original.subject : `Re: ${original.subject || "(без темы)"}`);
|
|
5261
5304
|
const result = await yandexMailSend({
|
|
5262
5305
|
to,
|
|
5263
5306
|
subject,
|
|
@@ -5638,7 +5681,7 @@ function parseImapFetchSummaries(text, options = {}) {
|
|
|
5638
5681
|
const subject = headers.subject || "";
|
|
5639
5682
|
const from = headers.from || "";
|
|
5640
5683
|
const date = headers.date || "";
|
|
5641
|
-
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0,
|
|
5684
|
+
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 3000);
|
|
5642
5685
|
rows.push({
|
|
5643
5686
|
uid,
|
|
5644
5687
|
date,
|
|
@@ -5769,12 +5812,55 @@ function decodeEmbeddedBase64MailBody(value) {
|
|
|
5769
5812
|
for (const match of matches) {
|
|
5770
5813
|
const clean = match.replace(/\s+/g, "");
|
|
5771
5814
|
if (!/^[A-Z0-9+/]+={0,2}$/iu.test(clean) || clean.length < 160) continue;
|
|
5772
|
-
const candidate =
|
|
5815
|
+
const candidate = decodeBase64Utf8Candidate(clean);
|
|
5773
5816
|
if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(candidate)) return candidate;
|
|
5774
5817
|
}
|
|
5775
5818
|
return text;
|
|
5776
5819
|
}
|
|
5777
5820
|
|
|
5821
|
+
function decodeBase64Utf8Candidate(value) {
|
|
5822
|
+
const clean = String(value || "").replace(/\s+/g, "");
|
|
5823
|
+
const padded = clean + "=".repeat((4 - (clean.length % 4)) % 4);
|
|
5824
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5827
|
+
function looksLikeEncodedMailText(value) {
|
|
5828
|
+
const text = String(value || "").trim();
|
|
5829
|
+
if (!text) return false;
|
|
5830
|
+
const compact = text.replace(/\s+/g, "");
|
|
5831
|
+
if (compact.length < 80) return false;
|
|
5832
|
+
if (/^[A-Z0-9+/]+={0,2}$/iu.test(compact)) return true;
|
|
5833
|
+
const encodedChars = (text.match(/[A-Z0-9+/=]/giu) || []).length;
|
|
5834
|
+
return encodedChars / Math.max(text.length, 1) > 0.82 && !/[А-Яа-яЁё]{3,}/u.test(text);
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5837
|
+
function cleanupDecodedMailText(value) {
|
|
5838
|
+
return String(value || "")
|
|
5839
|
+
.replace(/<style\b[\s\S]*?<\/style>/giu, " ")
|
|
5840
|
+
.replace(/<script\b[\s\S]*?<\/script>/giu, " ")
|
|
5841
|
+
.replace(/<[^>]+>/g, " ")
|
|
5842
|
+
.replace(/https?:\/\/\S+/giu, " ")
|
|
5843
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]+/gu, " ")
|
|
5844
|
+
.replace(/[^\S\n]+/g, " ")
|
|
5845
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
5846
|
+
.trim();
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
function normalizeMailSnippetForDisplay(value, maxLength = 600) {
|
|
5850
|
+
let text = String(value || "").trim();
|
|
5851
|
+
if (!text) return "";
|
|
5852
|
+
if (looksLikeEncodedMailText(text)) {
|
|
5853
|
+
const compact = text.replace(/\s+/g, "");
|
|
5854
|
+
const decoded = cleanupDecodedMailText(decodeBase64Utf8Candidate(compact));
|
|
5855
|
+
if (/[А-Яа-яЁё]{3,}/u.test(decoded) || /[A-ZА-Яа-яЁё]{3,}\s+[A-ZА-Яа-яЁё]{3,}/u.test(decoded)) {
|
|
5856
|
+
text = decoded;
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
text = cleanupDecodedMailText(decodeEmbeddedBase64MailBody(text));
|
|
5860
|
+
if (looksLikeEncodedMailText(text)) return "";
|
|
5861
|
+
return text.replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5778
5864
|
async function yandexDavRequest(url, token, options = {}) {
|
|
5779
5865
|
const response = await fetch(url, {
|
|
5780
5866
|
method: options.method || "GET",
|
|
@@ -12568,8 +12654,11 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
12568
12654
|
if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) {
|
|
12569
12655
|
const reply = parseYandexMailReplyRequest(question, previousAssistantText);
|
|
12570
12656
|
if (!reply.uid || !reply.text) return "Для ответа укажите письмо и текст. Пример: ответь на письмо #2382 текст: спасибо, получил.";
|
|
12657
|
+
if (/(пометь|отметь|сделай)/iu.test(normalized) && /(прочитан)/iu.test(normalized)) {
|
|
12658
|
+
await yandexMailMark(reply.uid, !/непрочитан/iu.test(normalized), { mailbox: await resolveYandexMailbox(reply.mailbox || "INBOX") });
|
|
12659
|
+
}
|
|
12571
12660
|
const result = await yandexMailReply({ ...reply, confirm: true });
|
|
12572
|
-
return
|
|
12661
|
+
return `Письмо #${result.replyToUid} обработано. Ответ отправлен: ${result.to.join(", ")}. Тема: ${result.subject}.`;
|
|
12573
12662
|
}
|
|
12574
12663
|
if (/(перешли|переслать|перешли\s+письмо|fwd|forward)/iu.test(normalized)) {
|
|
12575
12664
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
@@ -12678,6 +12767,26 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
12678
12767
|
latest[0] ? `Самое свежее: ${formatYandexMailSummary(latest[0])}` : "",
|
|
12679
12768
|
].filter(Boolean).join("\n");
|
|
12680
12769
|
}
|
|
12770
|
+
const recentHours = extractRecentMailHours(question);
|
|
12771
|
+
if (recentHours) {
|
|
12772
|
+
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
12773
|
+
const rows = await yandexMailList({ mailbox, limit: 50, unread: /непрочитан/iu.test(normalized) });
|
|
12774
|
+
const since = Date.now() - recentHours * 60 * 60 * 1000;
|
|
12775
|
+
const recentRows = rows.filter((row) => {
|
|
12776
|
+
const time = Date.parse(row.date || "");
|
|
12777
|
+
return Number.isFinite(time) && time >= since;
|
|
12778
|
+
});
|
|
12779
|
+
if (!recentRows.length) return `За последние ${recentHours} ${formatRussianHours(recentHours)} писем не найдено.`;
|
|
12780
|
+
const detailedRows = [];
|
|
12781
|
+
for (const row of recentRows.slice(0, 10)) {
|
|
12782
|
+
const detailed = await yandexMailRead(row.uid, { mailbox, markSeen: false }).catch(() => null);
|
|
12783
|
+
detailedRows.push(detailed && detailed.status !== "not-found" ? { ...row, ...detailed } : row);
|
|
12784
|
+
}
|
|
12785
|
+
return [
|
|
12786
|
+
`За последние ${recentHours} ${formatRussianHours(recentHours)} нашел ${recentRows.length} ${formatRussianLetters(recentRows.length)}:`,
|
|
12787
|
+
...detailedRows.map((row, index) => formatYandexMailDigestItem(row, index + 1)),
|
|
12788
|
+
].join("\n");
|
|
12789
|
+
}
|
|
12681
12790
|
if (isYandexMailReadRequest(normalized)) {
|
|
12682
12791
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText)
|
|
12683
12792
|
|| await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
|
|
@@ -12686,6 +12795,7 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
12686
12795
|
if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
|
|
12687
12796
|
return formatYandexMailRead(row);
|
|
12688
12797
|
}
|
|
12798
|
+
|
|
12689
12799
|
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
12690
12800
|
const rows = /(найди|поиск)/iu.test(normalized)
|
|
12691
12801
|
? await yandexMailSearch(cleanupYandexQuery(question), { mailbox, limit: 10 })
|
|
@@ -12892,7 +13002,7 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
12892
13002
|
}
|
|
12893
13003
|
} catch (error) {
|
|
12894
13004
|
const message = error instanceof Error ? error.message : String(error);
|
|
12895
|
-
if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT
|
|
13005
|
+
if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT нужны|Не знаю адрес|Геокодер вернул неподходящий адрес)/u.test(message)) return message;
|
|
12896
13006
|
return `Не смог выполнить запрос к сервисам Яндекса: ${message}`;
|
|
12897
13007
|
}
|
|
12898
13008
|
return "";
|
|
@@ -13191,7 +13301,7 @@ function resolveYandexMailUidFromQuestion(question, previousAssistantText = "")
|
|
|
13191
13301
|
const uid = extractYandexMailUidByOrdinal(previousAssistantText, Number(actionOrdinal));
|
|
13192
13302
|
if (uid) return uid;
|
|
13193
13303
|
}
|
|
13194
|
-
if (/(самое\s+свеж|последн|получи|получить|текст\s+(?:то\s+)?(?:письм|где)
|
|
13304
|
+
if (/(самое\s+свеж|последн|получи|получить|текст\s+(?:то\s+)?(?:письм|где)|содержим|ответь|ответить|им\b|ему\b|ей\b|пометь|отметь|сделай|удали|удалить)/iu.test(String(question || ""))) {
|
|
13195
13305
|
return extractFirstYandexMailUid(previousAssistantText);
|
|
13196
13306
|
}
|
|
13197
13307
|
return 0;
|
|
@@ -13226,10 +13336,12 @@ function isExplicitYandexDiskPathDelete(question) {
|
|
|
13226
13336
|
function parseYandexMailReplyRequest(question, previousAssistantText = "") {
|
|
13227
13337
|
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
13228
13338
|
const uid = resolveYandexMailUidFromQuestion(text, previousAssistantText);
|
|
13339
|
+
const subjectMatch = text.match(/(?:тема|subject)\s*:\s*(.*?)(?=\s+(?:текст|body|сообщение)\s*:|$)/iu);
|
|
13229
13340
|
const bodyMatch = text.match(/(?:текст|body|сообщение)\s*:\s*(.*)$/iu)
|
|
13230
13341
|
|| text.match(/(?:ответь|ответить|напиши\s+ответ)(?:\s+на\s+письмо\s+#?\d+|\s+#?\d+)?\s*:?\s*(.*)$/iu);
|
|
13231
13342
|
return {
|
|
13232
13343
|
uid,
|
|
13344
|
+
subject: (subjectMatch?.[1] || "").trim(),
|
|
13233
13345
|
text: (bodyMatch?.[1] || "").trim(),
|
|
13234
13346
|
mailbox: extractYandexMailboxName(question) || "INBOX",
|
|
13235
13347
|
};
|
|
@@ -13327,8 +13439,19 @@ function formatYandexMailSummary(row) {
|
|
|
13327
13439
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
13328
13440
|
}
|
|
13329
13441
|
|
|
13442
|
+
function formatYandexMailDigestItem(row, index) {
|
|
13443
|
+
const snippet = normalizeMailSnippetForDisplay(row.snippet, 350);
|
|
13444
|
+
return [
|
|
13445
|
+
`${index}. Письмо #${row.uid}`,
|
|
13446
|
+
` От: ${row.from || "-"}`,
|
|
13447
|
+
` Тема: ${row.subject || "(без темы)"}`,
|
|
13448
|
+
row.date ? ` Дата: ${row.date}` : "",
|
|
13449
|
+
snippet ? ` О чем: ${snippet}` : " О чем: текст письма не распознан, доступна только тема.",
|
|
13450
|
+
].filter(Boolean).join("\n");
|
|
13451
|
+
}
|
|
13452
|
+
|
|
13330
13453
|
function formatYandexMailRead(row) {
|
|
13331
|
-
const body =
|
|
13454
|
+
const body = normalizeMailSnippetForDisplay(row.snippet, 2000);
|
|
13332
13455
|
return [
|
|
13333
13456
|
`Письмо #${row.uid}`,
|
|
13334
13457
|
`От: ${row.from || "-"}`,
|
|
@@ -13339,6 +13462,35 @@ function formatYandexMailRead(row) {
|
|
|
13339
13462
|
].filter((line) => line !== "").join("\n");
|
|
13340
13463
|
}
|
|
13341
13464
|
|
|
13465
|
+
function extractRecentMailHours(question) {
|
|
13466
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
13467
|
+
if (!/(последн|прошедш|за\s+\d+|за\s+два|за\s+три|за\s+час)/iu.test(text)) return 0;
|
|
13468
|
+
if (!/(час|минут|сут|день|дня|дней)/iu.test(text)) return 0;
|
|
13469
|
+
const numeric = Number(text.match(/(\d+)\s*(?:час|часа|часов)/iu)?.[1] || 0);
|
|
13470
|
+
if (numeric > 0) return numeric;
|
|
13471
|
+
const words = { один: 1, одну: 1, два: 2, две: 2, три: 3, четыре: 4, пять: 5, шесть: 6, семь: 7, восемь: 8, девять: 9, десять: 10 };
|
|
13472
|
+
const word = text.match(/(?:^|\s)(один|одну|два|две|три|четыре|пять|шесть|семь|восемь|девять|десять)\s*(?:час|часа|часов)(?:\s|$|[,.!?;:])/iu)?.[1];
|
|
13473
|
+
if (word && words[word]) return words[word];
|
|
13474
|
+
const minutes = Number(text.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 0);
|
|
13475
|
+
if (minutes > 0) return Math.max(1, Math.ceil(minutes / 60));
|
|
13476
|
+
if (/сутк|день|дня|дней/u.test(text)) return 24;
|
|
13477
|
+
return /час/u.test(text) ? 1 : 0;
|
|
13478
|
+
}
|
|
13479
|
+
|
|
13480
|
+
function formatRussianHours(value) {
|
|
13481
|
+
const n = Math.abs(Number(value || 0));
|
|
13482
|
+
if (n % 10 === 1 && n % 100 !== 11) return "час";
|
|
13483
|
+
if ([2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100)) return "часа";
|
|
13484
|
+
return "часов";
|
|
13485
|
+
}
|
|
13486
|
+
|
|
13487
|
+
function formatRussianLetters(value) {
|
|
13488
|
+
const n = Math.abs(Number(value || 0));
|
|
13489
|
+
if (n % 10 === 1 && n % 100 !== 11) return "письмо";
|
|
13490
|
+
if ([2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100)) return "письма";
|
|
13491
|
+
return "писем";
|
|
13492
|
+
}
|
|
13493
|
+
|
|
13342
13494
|
function slugForFile(value) {
|
|
13343
13495
|
return String(value || "")
|
|
13344
13496
|
.toLocaleLowerCase("ru-RU")
|