feat(reminders): получатели в отложенных/входящих, приватные сообщения чата, календарь, бейдж уведомлений

This commit is contained in:
2026-09-21 10:26:53 +03:00
parent 17e6ebf412
commit aa316141be
20 changed files with 312 additions and 36 deletions

View File

@@ -444,7 +444,9 @@ export class NotificationService {
formId: data.formId.toString(),
isChat: isChatNotification ? 'true' : 'false',
...(data.messageId ? { messageId: data.messageId.toString() } : {}),
type: 'chat',
// Чатовые уведомления сохраняем с type 'chat' (SW-обработчики кнопок «Ответить»/«Прочитано»),
// остальные — с реальным типом, чтобы не глушились suppression-логикой активного чата
type: isChatNotification ? 'chat' : (data.type || 'notification'),
}
: undefined;

View File

@@ -27,13 +27,14 @@ export interface SendTaskMessageParams {
botId?: number | null; // сообщение от бота: authorId=null, botId, messageType='bot'
organizationId: number;
message: string;
messageType?: string; // 'comment' (default), 'bot', 'system', ...
messageType?: string; // 'comment' (default), 'bot', 'system', 'status_change', 'reminder', ...
replyToMessageId?: number | null;
mentionedUserIds?: number[] | null;
mentionedBotIds?: number[] | null;
attachments?: MessageAttachment[] | null;
bodyAuthorId?: number | null; // authorId из тела запроса (null — признак бот/системного сообщения)
isBotToken?: boolean; // запрос аутентифицирован bot-service JWT
visibleToUserIds?: number[] | null; // приватность: только перечисленные пользователи видят сообщение (NULL — все участники чата)
}
// Создаёт сообщение задачи со всеми side-эффектами:
@@ -47,10 +48,10 @@ export interface SendTaskMessageParams {
export async function sendTaskMessage(params: SendTaskMessageParams): Promise<TaskMessage> {
const { task, user, organizationId } = params;
const botId = params.botId ?? null;
// Системные сообщения ('system', 'status_change') могут быть без автора —
// их пишут автоматизации (ctx.tasks.sendMessage), автор в чате — «Система»
// Системные сообщения ('system', 'status_change', 'reminder') могут быть без автора —
// их пишут автоматизации (ctx.tasks.sendMessage, напоминания), автор в чате — «Система»
const isAuthorlessSystem = !user && !botId &&
(params.messageType === 'system' || params.messageType === 'status_change');
(params.messageType === 'system' || params.messageType === 'status_change' || params.messageType === 'reminder');
if (!user && !botId && !isAuthorlessSystem) {
throw new SendTaskMessageError('Не указан автор сообщения (user или botId)', 500);
}
@@ -105,6 +106,9 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
messageType,
mentionedUserIds: mentionedUserIds.length > 0 ? mentionedUserIds : null,
attachments: params.attachments?.length ? params.attachments : null,
visibleToUserIds: params.visibleToUserIds && params.visibleToUserIds.length > 0
? [...new Set(params.visibleToUserIds)]
: null,
};
const createdMessage = await storage.createTaskMessage(messageData, organizationId);
@@ -232,12 +236,30 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
}
}
eventBus.publishEvent({
type: 'message_created',
data: { taskId: taskId, message: createdMessage },
organizationId,
taskId: taskId
});
// Приватные сообщения (visible_to_user_ids): SSE-событие получают только
// разрешённые пользователи (перечисленные + автор). Остальные участники чата
// событие не получают и сообщение не увидят ни в реальном времени, ни при загрузке
// (фильтр в storage.getTaskMessages по viewerUserId).
const visibleTo = messageData.visibleToUserIds;
if (visibleTo && visibleTo.length > 0) {
const allowed = new Set<number>(visibleTo);
if (user?.id) allowed.add(user.id);
for (const uid of allowed) {
eventBus.publishEvent({
type: 'message_created',
data: { taskId: taskId, message: createdMessage },
organizationId,
userId: uid,
});
}
} else {
eventBus.publishEvent({
type: 'message_created',
data: { taskId: taskId, message: createdMessage },
organizationId,
taskId: taskId
});
}
// Уведомления и исходящие вебхуки — только для сообщений от пользователя
// (для bot-сообщений — как в POST /api/bot/message: без notificationService).