@mikitasazan/notify 1.1.0 → 1.1.2

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/cli.js CHANGED
@@ -163,8 +163,7 @@ else {
163
163
  url: one('url'),
164
164
  target: one('target'),
165
165
  via: one('via'),
166
- note: one('note'),
167
- key: one('key')
166
+ note: one('note')
168
167
  };
169
168
  break;
170
169
  case 'job':
@@ -176,8 +175,7 @@ else {
176
175
  stats: pairs('stat'),
177
176
  items: items(),
178
177
  note: one('note'),
179
- url: one('url'),
180
- key: one('key')
178
+ url: one('url')
181
179
  };
182
180
  break;
183
181
  case 'report':
@@ -188,8 +186,7 @@ else {
188
186
  period: one('period'),
189
187
  lines: pairs('line'),
190
188
  items: items(),
191
- url: one('url'),
192
- key: one('key')
189
+ url: one('url')
193
190
  };
194
191
  break;
195
192
  case 'ci':
@@ -200,8 +197,7 @@ else {
200
197
  branch: one('branch'),
201
198
  commit: one('commit'),
202
199
  actor: one('actor'),
203
- url: one('url'),
204
- key: one('key')
200
+ url: one('url')
205
201
  };
206
202
  break;
207
203
  case 'pr':
@@ -213,8 +209,7 @@ else {
213
209
  title: one('title') ?? '(без заголовка)',
214
210
  author: one('author'),
215
211
  reviewer: one('reviewer'),
216
- url: one('url'),
217
- key: one('key')
212
+ url: one('url')
218
213
  };
219
214
  break;
220
215
  case 'issue':
@@ -226,8 +221,7 @@ else {
226
221
  title: one('title') ?? '(без заголовка)',
227
222
  author: one('author'),
228
223
  assignee: one('assignee'),
229
- url: one('url'),
230
- key: one('key')
224
+ url: one('url')
231
225
  };
232
226
  break;
233
227
  case 'incident':
@@ -236,8 +230,7 @@ else {
236
230
  project: project(),
237
231
  title: one('title') ?? '(без заголовка)',
238
232
  detail: one('detail'),
239
- url: one('url'),
240
- key: one('key')
233
+ url: one('url')
241
234
  };
242
235
  break;
243
236
  case 'heartbeat_miss':
@@ -246,8 +239,7 @@ else {
246
239
  project: project(),
247
240
  job: one('job') ?? '(без имени)',
248
241
  lastSeen: one('last-seen'),
249
- expected: one('expected'),
250
- key: one('key')
242
+ expected: one('expected')
251
243
  };
252
244
  break;
253
245
  case 'file': {
@@ -261,8 +253,7 @@ else {
261
253
  title: one('title') ?? '(без заголовка)',
262
254
  path: path ?? '',
263
255
  filename: one('filename'),
264
- note: one('note'),
265
- key: one('key')
256
+ note: one('note')
266
257
  };
267
258
  break;
268
259
  }
@@ -270,6 +261,12 @@ else {
270
261
  log(`неизвестный тип события: ${command ?? '(не указан)'}`);
271
262
  }
272
263
  }
264
+ // --key применим к любому типу — одна точка вместо строки в каждом case
265
+ // (девять копий уже потеряли бы десятую). `??`: путь --json может нести
266
+ // key в самом объекте, отсутствие флага не должно его затирать.
267
+ if (event) {
268
+ event.key = one('key') ?? event.key;
269
+ }
273
270
  // Ошибки разбора — до отправки: лучше внятно сказать, что не так с командой,
274
271
  // чем прислать сообщение с «true» вместо ссылки или 🔴 на успешном деплое.
275
272
  if (parseErrors.length > 0) {
package/dist/events.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * - обязательные поля не добавляются никогда — только новый тип события.
10
10
  * Тогда старый вызывающий код и новый пакет совместимы в обе стороны.
11
11
  */
12
- export type Project = 'playhub' | 'one-q' | 'arvent' | 'game-publisher' | 'vault' | 'mac-config';
12
+ export type Project = 'playhub' | 'one-q' | 'arvent' | 'game-publisher' | 'vault' | 'mac-config' | 'alitools';
13
13
  /**
14
14
  * Стабильный машинный ключ задачи — последняя строка каждой карточки, вида
15
15
  * `#проект/ключ`. По нему дневной разборщик сверяет «это 🔴 уже закрыто более
package/dist/render.js CHANGED
@@ -175,7 +175,11 @@ const RENDERERS = {
175
175
  const slug = (raw) => raw
176
176
  .toLowerCase()
177
177
  .replace(/[^\p{L}\p{N}]+/gu, '-')
178
- .replace(/^-+|-+$/g, '');
178
+ .replace(/^-+|-+$/g, '')
179
+ // Ключ — идентификатор, не пересказ: без среза тег из длинного заголовка
180
+ // съедал бюджет caption до отрицательного, и slice с минусом возвращал
181
+ // почти весь текст — Telegram отвечал постоянным 400, файл терялся.
182
+ .slice(0, 60);
179
183
  export const eventKey = (e) => {
180
184
  const fallback = () => {
181
185
  switch (e.type) {
@@ -215,6 +219,6 @@ export const render = (e) => {
215
219
  // clampMessage может выйти за переданный limit на хвост закрывающих тегов и
216
220
  // многоточие — минус 40 оставляет ему этот запас. У сообщений свой запас уже
217
221
  // есть (4000 против 4096 у Telegram), у caption лимит 1024 настоящий.
218
- const budget = e.type === 'file' ? 1024 - tag.length - 40 : 4000 - tag.length - 1;
222
+ const budget = Math.max(64, e.type === 'file' ? 1024 - tag.length - 40 : 4000 - tag.length - 1);
219
223
  return `${clampMessage(renderer(e), budget)}\n${tag}`;
220
224
  };
package/dist/routes.js CHANGED
@@ -12,7 +12,12 @@ export const ROUTES = {
12
12
  vault: { chat: '-1004459314999', ops: 3 },
13
13
  // Тоже инфраструктура, без `dev`: дневной дайджест задач и понедельничная
14
14
  // сводка спотыканий. До этой строки оба тихо умирали на «неизвестный проект».
15
- 'mac-config': { chat: '-1004442522004', ops: 2 }
15
+ 'mac-config': { chat: '-1004442522004', ops: 2 },
16
+ // Корпоративный проект владельца: только его собственные отчёты «что дала
17
+ // выливка» (ali98x-sentry). Без `dev` — команда проекта живёт в чужих
18
+ // системах, людей здесь нет. До 18.08.2026 отчёты терялись на «неизвестный
19
+ // проект» неделями.
20
+ alitools: { chat: '-1003904331479', ops: 3 }
16
21
  };
17
22
  /**
18
23
  * Куда уходит событие. Всё — во вкладку «Ops» своего проекта; красное
package/dist/send.d.ts CHANGED
@@ -1,16 +1,5 @@
1
1
  import type { NotifyEvent, Project } from './events.ts';
2
2
  export type SendResult = 'sent' | 'skipped' | 'failed';
3
- /**
4
- * Отправляет событие во все его цели (тема проекта + при необходимости
5
- * `incidents` + чат команды). Цели идут последовательно; провал одной не
6
- * отменяет остальные. `'sent'`, если хотя бы одна цель получила сообщение.
7
- *
8
- * Неизвестный проект по-прежнему НЕ роняет вызвавший крон (код возврата не
9
- * меняется) — но и не исчезает молча: в mac-config Ops уходит красная
10
- * карточка. Дважды этот класс провала жил незамеченным неделями: «vault» и
11
- * «mac-config» до 04.08, отчёты Alitools — до 18.08. Рекурсия невозможна:
12
- * карточка-ошибка адресована mac-config, который в ROUTES есть всегда.
13
- */
14
3
  export declare const notify: (e: NotifyEvent) => Promise<SendResult>;
15
4
  /**
16
5
  * Готовый HTML во вкладку «Ops» проекта — ТОЛЬКО для дневных отчётов.
@@ -29,4 +18,4 @@ export declare const notify: (e: NotifyEvent) => Promise<SendResult>;
29
18
  *
30
19
  * Всегда беззвучно: отчёт читают утром, а не по звонку.
31
20
  */
32
- export declare const sendReport: (project: Project, html: string) => Promise<SendResult>;
21
+ export declare const sendReport: (project: Project, html: string, key?: string) => Promise<SendResult>;
package/dist/send.js CHANGED
@@ -224,24 +224,28 @@ const sendFile = async (e) => {
224
224
  return 'skipped';
225
225
  }
226
226
  const caption = render(e);
227
+ const results = [];
228
+ // Контракт тот же, что у deliver: провал одной цели не отменяет остальные.
227
229
  for (const target of where) {
228
230
  let waitMs = 0;
231
+ let got = 'failed';
229
232
  for (let i = 0; i < MAX_ATTEMPTS; i++) {
230
233
  if (waitMs > 0) {
231
234
  await sleep(waitMs);
232
235
  }
233
236
  const result = await sendFileOnce(token, target, e, caption);
234
237
  if (result.outcome === 'ok') {
235
- return 'sent';
238
+ got = 'sent';
239
+ break;
236
240
  }
237
241
  if (result.outcome === 'fail') {
238
- return 'failed';
242
+ break;
239
243
  }
240
244
  waitMs = result.waitMs;
241
245
  }
246
+ results.push(got);
242
247
  }
243
- log('исчерпаны попытки отправки файла');
244
- return 'failed';
248
+ return results.includes('sent') ? 'sent' : 'failed';
245
249
  };
246
250
  /**
247
251
  * Отправляет событие во все его цели (тема проекта + при необходимости
@@ -254,17 +258,25 @@ const sendFile = async (e) => {
254
258
  * «mac-config» до 04.08, отчёты Alitools — до 18.08. Рекурсия невозможна:
255
259
  * карточка-ошибка адресована mac-config, который в ROUTES есть всегда.
256
260
  */
261
+ const reportLostProject = async (project, kind) => {
262
+ // Локальный лог называет и допустимые написания — это единственная
263
+ // диагностика, доступная на машине, где случилась опечатка.
264
+ log(`неизвестный проект «${String(project)}» — известны: ${Object.keys(ROUTES).join(', ')}`);
265
+ const lost = {
266
+ type: 'job',
267
+ project: 'mac-config',
268
+ job: 'notify: событие потеряно',
269
+ status: 'fail',
270
+ note: `проект «${String(project)}» не в ROUTES — событие «${kind}» никуда не доставлено`,
271
+ key: 'notify-unknown-project'
272
+ };
273
+ await deliver(targets(lost), render(lost)).catch(() => undefined);
274
+ };
257
275
  export const notify = async (e) => {
258
- if (!(e.project in ROUTES)) {
259
- const lost = {
260
- type: 'job',
261
- project: 'mac-config',
262
- job: 'notify: событие потеряно',
263
- status: 'fail',
264
- note: `проект «${String(e.project)}» не в ROUTES — событие «${String(e.type)}» никуда не доставлено`,
265
- key: 'notify-unknown-project'
266
- };
267
- await deliver(targets(lost), render(lost)).catch(() => undefined);
276
+ // Object.hasOwn, не `in`: `in` ходит по цепочке прототипов, и --project
277
+ // toString/constructor проходил бы гвард, терял событие И карточку о потере.
278
+ if (!Object.hasOwn(ROUTES, e.project)) {
279
+ await reportLostProject(e.project, String(e.type));
268
280
  return 'skipped';
269
281
  }
270
282
  if (e.type === 'file') {
@@ -289,11 +301,14 @@ export const notify = async (e) => {
289
301
  *
290
302
  * Всегда беззвучно: отчёт читают утром, а не по звонку.
291
303
  */
292
- export const sendReport = async (project, html) => {
293
- const forum = ROUTES[project];
294
- if (!forum) {
295
- log(`неизвестный проект «${project}» отчёт не отправлен`);
304
+ export const sendReport = async (project, html, key) => {
305
+ if (!Object.hasOwn(ROUTES, project)) {
306
+ // Та же красная карточка, что у notify(): дверь свободного HTML — не
307
+ // лазейка для молчаливой потери (этот класс уже жил неделями дважды).
308
+ await reportLostProject(project, 'report');
296
309
  return 'skipped';
297
310
  }
298
- return deliver([{ chat: forum.chat, thread: forum.ops, silent: true }], clampMessage(html));
311
+ const forum = ROUTES[project];
312
+ const tag = key ? `\n<code>#${project}/${key}</code>` : '';
313
+ return deliver([{ chat: forum.chat, thread: forum.ops, silent: true }], clampMessage(html) + tag);
299
314
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",