@mikitasazan/notify 1.1.2 → 1.2.0
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/dist/events.d.ts +9 -6
- package/dist/render.js +36 -16
- package/dist/send.js +3 -1
- package/package.json +1 -1
package/dist/events.d.ts
CHANGED
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
export type Project = 'playhub' | 'one-q' | 'arvent' | 'game-publisher' | 'vault' | 'mac-config' | 'alitools';
|
|
13
13
|
/**
|
|
14
14
|
* Стабильный машинный ключ задачи — последняя строка каждой карточки, вида
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* `#ключ` (без названия проекта: карточка и так лежит в форуме своего
|
|
16
|
+
* проекта — `targets()` не шлёт её в чужой). По нему дневной разборщик
|
|
17
|
+
* сверяет «это 🔴 уже закрыто более поздней карточкой того же ключа?» без
|
|
18
|
+
* сравнения человеческих формулировок, которые меняются. Необязателен: без
|
|
19
|
+
* него ключ выводится из типа и заголовка (см. `render.ts`), но выведенный
|
|
20
|
+
* наследует хрупкость формулировки — наши регулярные отправители передают
|
|
21
|
+
* его явно. В вывод CLI (stderr, и его же сторож на VPS читает объединённым
|
|
22
|
+
* потоком по словам `sent|failed|skipped`) ключ не попадает никогда — новое
|
|
23
|
+
* слово там ослепило бы сторожа.
|
|
21
24
|
*/
|
|
22
25
|
type Keyed = {
|
|
23
26
|
key?: string;
|
package/dist/render.js
CHANGED
|
@@ -36,7 +36,10 @@ export const clampMessage = (text, limit = 4000) => {
|
|
|
36
36
|
}
|
|
37
37
|
const body = cut.slice(0, end);
|
|
38
38
|
// Кламп мог отрезать закрывающие теги — добираем их, чтобы разметка сошлась.
|
|
39
|
-
|
|
39
|
+
// blockquote — с приходом цитаты для примечаний/деталей длинный detail режется
|
|
40
|
+
// прямо посередине неё, и без этого тега Telegram отвечал бы 400 на незакрытую
|
|
41
|
+
// цитату (regex `<blockquote[ >]` ловит и вариант с атрибутом `expandable`).
|
|
42
|
+
const tail = ['b', 'a', 'i', 'code', 'blockquote']
|
|
40
43
|
.filter((t) => {
|
|
41
44
|
const opened = (body.match(new RegExp(`<${t}[ >]`, 'g')) ?? []).length;
|
|
42
45
|
const closed = (body.match(new RegExp(`</${t}>`, 'g')) ?? []).length;
|
|
@@ -47,10 +50,23 @@ export const clampMessage = (text, limit = 4000) => {
|
|
|
47
50
|
return `${body}${tail}\n…`;
|
|
48
51
|
};
|
|
49
52
|
const header = (icon, title, project) => `${icon} <b>${esc(title)}</b> · ${esc(project)}`;
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
53
|
+
// Тире, не жирное значение: сплошной жирный текст в первой версии карточки
|
|
54
|
+
// читался как крик (жалоба владельца 18.08). Иконка-лид уже держит внимание
|
|
55
|
+
// на заголовке, факты идут построчно и без выделения — глаз сам находит
|
|
56
|
+
// цифру рядом с меткой.
|
|
57
|
+
const kv = (label, value) => value === undefined || value === '' ? null : `${esc(label)} — ${esc(value)}`;
|
|
58
|
+
// Длинное пояснение (примечание, детали инцидента) — цитатой: у Telegram это
|
|
59
|
+
// полоска слева и лёгкий отступ, читается как «подробности», а не как часть
|
|
60
|
+
// заголовка. Длиннее ~400 знаков — цитата сворачивается сама (`expandable`,
|
|
61
|
+
// Bot API), иначе стектрейс или дамп лога растягивает карточку на весь экран.
|
|
62
|
+
const EXPAND_AT = 400;
|
|
63
|
+
const note = (text) => {
|
|
64
|
+
if (!text) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const body = esc(text);
|
|
68
|
+
return body.length > EXPAND_AT ? `<blockquote expandable>${body}</blockquote>` : `<blockquote>${body}</blockquote>`;
|
|
69
|
+
};
|
|
54
70
|
const link = (url, label) => url ? `<a href="${esc(url)}">${esc(label)}</a>` : null;
|
|
55
71
|
const join = (parts) => parts.filter((p) => p !== null).join('\n');
|
|
56
72
|
/** Список позиций — общий для `job` и `report`, чтобы они не разъехались. */
|
|
@@ -70,7 +86,7 @@ const renderDeploy = (e) => {
|
|
|
70
86
|
commitLine,
|
|
71
87
|
kv('откуда', e.via),
|
|
72
88
|
kv('куда', e.target),
|
|
73
|
-
|
|
89
|
+
note(e.note),
|
|
74
90
|
link(e.url, 'Открыть логи')
|
|
75
91
|
]);
|
|
76
92
|
};
|
|
@@ -82,7 +98,7 @@ const renderJob = (e) => {
|
|
|
82
98
|
...(e.stats ?? []).map(([label, value]) => kv(label, value)),
|
|
83
99
|
items.length > 0 ? '' : null,
|
|
84
100
|
...items,
|
|
85
|
-
|
|
101
|
+
note(e.note),
|
|
86
102
|
link(e.url, 'Подробнее')
|
|
87
103
|
]);
|
|
88
104
|
};
|
|
@@ -146,14 +162,14 @@ const renderIssue = (e) => {
|
|
|
146
162
|
link(e.url, 'Открыть задачу')
|
|
147
163
|
]);
|
|
148
164
|
};
|
|
149
|
-
const renderIncident = (e) => join([header('🚨', 'Инцидент', e.project), esc(e.title),
|
|
165
|
+
const renderIncident = (e) => join([header('🚨', 'Инцидент', e.project), esc(e.title), note(e.detail), link(e.url, 'Подробнее')]);
|
|
150
166
|
const renderHeartbeatMiss = (e) => join([
|
|
151
167
|
header('🔴', `Не отметилась: ${e.job}`, e.project),
|
|
152
168
|
kv('последний раз', e.lastSeen),
|
|
153
169
|
kv('ожидалось', e.expected)
|
|
154
170
|
]);
|
|
155
171
|
// Подпись файла — та же карточка, но лимит Telegram у caption свой: 1024.
|
|
156
|
-
const renderFile = (e) => join([header('📄', e.title, e.project),
|
|
172
|
+
const renderFile = (e) => join([header('📄', e.title, e.project), note(e.note)]);
|
|
157
173
|
const RENDERERS = {
|
|
158
174
|
deploy: renderDeploy,
|
|
159
175
|
job: renderJob,
|
|
@@ -166,11 +182,15 @@ const RENDERERS = {
|
|
|
166
182
|
file: renderFile
|
|
167
183
|
};
|
|
168
184
|
/**
|
|
169
|
-
* Ключ задачи — последняя строка карточки:
|
|
170
|
-
* `
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
185
|
+
* Ключ задачи — последняя строка карточки: `#ключ` курсивом в <code>. Без
|
|
186
|
+
* названия проекта: `targets()` никогда не шлёт карточку в чужой форум,
|
|
187
|
+
* проект и так на виду в заголовке («· mac-config»), а дублирующий префикс
|
|
188
|
+
* только растягивал тег на лишнюю строку в узком экране телефона (жалоба
|
|
189
|
+
* владельца 18.08). Явный `key` побеждает; выведенный строится из заголовка
|
|
190
|
+
* и наследует хрупкость формулировки — регулярные отправители передают явный.
|
|
191
|
+
* Ключ переживает MTProto-чтение (простой текст, не разметка), по нему
|
|
192
|
+
* разборщик сверяет 🔴 с более поздней успешной карточкой той же задачи —
|
|
193
|
+
* внутри чата одного проекта, где ключ и так уникален.
|
|
174
194
|
*/
|
|
175
195
|
const slug = (raw) => raw
|
|
176
196
|
.toLowerCase()
|
|
@@ -198,9 +218,9 @@ export const eventKey = (e) => {
|
|
|
198
218
|
return e.type;
|
|
199
219
|
}
|
|
200
220
|
};
|
|
201
|
-
return
|
|
221
|
+
return e.key ? slug(e.key) : fallback();
|
|
202
222
|
};
|
|
203
|
-
const keyLine = (e) => `<code
|
|
223
|
+
const keyLine = (e) => `<i><code>#${esc(eventKey(e))}</code></i>`;
|
|
204
224
|
/**
|
|
205
225
|
* Рендерит событие в готовый HTML-текст, обрезанный под лимит Telegram.
|
|
206
226
|
* Ключ добавляется ПОСЛЕ обрезки, с зарезервированным местом: обрезанная
|
package/dist/send.js
CHANGED
|
@@ -309,6 +309,8 @@ export const sendReport = async (project, html, key) => {
|
|
|
309
309
|
return 'skipped';
|
|
310
310
|
}
|
|
311
311
|
const forum = ROUTES[project];
|
|
312
|
-
|
|
312
|
+
// Без проекта — как и render.ts: карточка уже лежит в форуме своего
|
|
313
|
+
// проекта, дублировать его в теге незачем.
|
|
314
|
+
const tag = key ? `\n<i><code>#${key}</code></i>` : '';
|
|
313
315
|
return deliver([{ chat: forum.chat, thread: forum.ops, silent: true }], clampMessage(html) + tag);
|
|
314
316
|
};
|
package/package.json
CHANGED