@mikitasazan/notify 1.1.3 → 1.2.1
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 -7
- package/dist/render.js +50 -17
- package/dist/send.js +3 -1
- package/package.json +1 -1
package/dist/events.d.ts
CHANGED
|
@@ -12,13 +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
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* `#ключ` (без названия проекта: карточка и так лежит в форуме своего
|
|
16
|
+
* проекта — `targets()` не шлёт её в чужой). По нему дневной разборщик
|
|
17
|
+
* сверяет «это 🔴 уже закрыто более поздней карточкой того же ключа?» без
|
|
18
|
+
* сравнения человеческих формулировок, которые меняются. Необязателен: без
|
|
19
|
+
* него ключ выводится из типа и заголовка (см. `render.ts`), но выведенный
|
|
20
|
+
* наследует хрупкость формулировки — наши регулярные отправители передают
|
|
21
|
+
* его явно. В вывод CLI (stderr, и его же сторож на VPS читает объединённым
|
|
22
|
+
* потоком по словам `sent|failed|skipped`) ключ не попадает никогда — новое
|
|
23
|
+
* слово там ослепило бы сторожа.
|
|
22
24
|
*/
|
|
23
25
|
type Keyed = {
|
|
24
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,36 @@ 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
|
+
//
|
|
58
|
+
// Только первая строка: однострочное поле по контракту (коммит, ветка,
|
|
59
|
+
// автор, статистика), а не место для абзаца. Живой случай (18.08): CI-карточка
|
|
60
|
+
// понесла ПОЛНОЕ тело коммита с историей под-коммитов через `--commit` и вместо
|
|
61
|
+
// одной строки развернулась на 3000 символов — многострочный текст либо
|
|
62
|
+
// ошибка вызывающего, либо должен идти через `note()`, а не молча раздувать
|
|
63
|
+
// карточку.
|
|
64
|
+
const firstLine = (value) => {
|
|
65
|
+
if (typeof value === 'number' || !value.includes('\n')) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
return `${value.split('\n')[0]}…`;
|
|
69
|
+
};
|
|
70
|
+
const kv = (label, value) => value === undefined || value === '' ? null : `${esc(label)} — ${esc(firstLine(value))}`;
|
|
71
|
+
// Длинное пояснение (примечание, детали инцидента) — цитатой: у Telegram это
|
|
72
|
+
// полоска слева и лёгкий отступ, читается как «подробности», а не как часть
|
|
73
|
+
// заголовка. Длиннее ~400 знаков — цитата сворачивается сама (`expandable`,
|
|
74
|
+
// Bot API), иначе стектрейс или дамп лога растягивает карточку на весь экран.
|
|
75
|
+
const EXPAND_AT = 400;
|
|
76
|
+
const note = (text) => {
|
|
77
|
+
if (!text) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const body = esc(text);
|
|
81
|
+
return body.length > EXPAND_AT ? `<blockquote expandable>${body}</blockquote>` : `<blockquote>${body}</blockquote>`;
|
|
82
|
+
};
|
|
54
83
|
const link = (url, label) => url ? `<a href="${esc(url)}">${esc(label)}</a>` : null;
|
|
55
84
|
const join = (parts) => parts.filter((p) => p !== null).join('\n');
|
|
56
85
|
/** Список позиций — общий для `job` и `report`, чтобы они не разъехались. */
|
|
@@ -62,7 +91,7 @@ const renderDeploy = (e) => {
|
|
|
62
91
|
// владельца на некликабельные дайджесты распространяется и сюда.
|
|
63
92
|
const commitLine = e.commit
|
|
64
93
|
? e.commitUrl
|
|
65
|
-
? `коммит: <a href="${esc(e.commitUrl)}"><b>${esc(e.commit)}</b></a>`
|
|
94
|
+
? `коммит: <a href="${esc(e.commitUrl)}"><b>${esc(firstLine(e.commit))}</b></a>`
|
|
66
95
|
: kv('коммит', e.commit)
|
|
67
96
|
: null;
|
|
68
97
|
return join([
|
|
@@ -70,7 +99,7 @@ const renderDeploy = (e) => {
|
|
|
70
99
|
commitLine,
|
|
71
100
|
kv('откуда', e.via),
|
|
72
101
|
kv('куда', e.target),
|
|
73
|
-
|
|
102
|
+
note(e.note),
|
|
74
103
|
link(e.url, 'Открыть логи')
|
|
75
104
|
]);
|
|
76
105
|
};
|
|
@@ -82,7 +111,7 @@ const renderJob = (e) => {
|
|
|
82
111
|
...(e.stats ?? []).map(([label, value]) => kv(label, value)),
|
|
83
112
|
items.length > 0 ? '' : null,
|
|
84
113
|
...items,
|
|
85
|
-
|
|
114
|
+
note(e.note),
|
|
86
115
|
link(e.url, 'Подробнее')
|
|
87
116
|
]);
|
|
88
117
|
};
|
|
@@ -146,14 +175,14 @@ const renderIssue = (e) => {
|
|
|
146
175
|
link(e.url, 'Открыть задачу')
|
|
147
176
|
]);
|
|
148
177
|
};
|
|
149
|
-
const renderIncident = (e) => join([header('🚨', 'Инцидент', e.project), esc(e.title),
|
|
178
|
+
const renderIncident = (e) => join([header('🚨', 'Инцидент', e.project), esc(e.title), note(e.detail), link(e.url, 'Подробнее')]);
|
|
150
179
|
const renderHeartbeatMiss = (e) => join([
|
|
151
180
|
header('🔴', `Не отметилась: ${e.job}`, e.project),
|
|
152
181
|
kv('последний раз', e.lastSeen),
|
|
153
182
|
kv('ожидалось', e.expected)
|
|
154
183
|
]);
|
|
155
184
|
// Подпись файла — та же карточка, но лимит Telegram у caption свой: 1024.
|
|
156
|
-
const renderFile = (e) => join([header('📄', e.title, e.project),
|
|
185
|
+
const renderFile = (e) => join([header('📄', e.title, e.project), note(e.note)]);
|
|
157
186
|
const RENDERERS = {
|
|
158
187
|
deploy: renderDeploy,
|
|
159
188
|
job: renderJob,
|
|
@@ -166,11 +195,15 @@ const RENDERERS = {
|
|
|
166
195
|
file: renderFile
|
|
167
196
|
};
|
|
168
197
|
/**
|
|
169
|
-
* Ключ задачи — последняя строка карточки:
|
|
170
|
-
* `
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
198
|
+
* Ключ задачи — последняя строка карточки: `#ключ` курсивом в <code>. Без
|
|
199
|
+
* названия проекта: `targets()` никогда не шлёт карточку в чужой форум,
|
|
200
|
+
* проект и так на виду в заголовке («· mac-config»), а дублирующий префикс
|
|
201
|
+
* только растягивал тег на лишнюю строку в узком экране телефона (жалоба
|
|
202
|
+
* владельца 18.08). Явный `key` побеждает; выведенный строится из заголовка
|
|
203
|
+
* и наследует хрупкость формулировки — регулярные отправители передают явный.
|
|
204
|
+
* Ключ переживает MTProto-чтение (простой текст, не разметка), по нему
|
|
205
|
+
* разборщик сверяет 🔴 с более поздней успешной карточкой той же задачи —
|
|
206
|
+
* внутри чата одного проекта, где ключ и так уникален.
|
|
174
207
|
*/
|
|
175
208
|
const slug = (raw) => raw
|
|
176
209
|
.toLowerCase()
|
|
@@ -198,9 +231,9 @@ export const eventKey = (e) => {
|
|
|
198
231
|
return e.type;
|
|
199
232
|
}
|
|
200
233
|
};
|
|
201
|
-
return
|
|
234
|
+
return e.key ? slug(e.key) : fallback();
|
|
202
235
|
};
|
|
203
|
-
const keyLine = (e) => `<code
|
|
236
|
+
const keyLine = (e) => `<i><code>#${esc(eventKey(e))}</code></i>`;
|
|
204
237
|
/**
|
|
205
238
|
* Рендерит событие в готовый HTML-текст, обрезанный под лимит Telegram.
|
|
206
239
|
* Ключ добавляется ПОСЛЕ обрезки, с зарезервированным местом: обрезанная
|
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