Атрибуция от бота в MCP/REST + REST API по ключу (этап 2)
- MCP: getActor (владелец ключа + бот), аудит changedByName=бот/label ключа, botId, metadata.source='mcp'; аудит для update_task/update_task_status - sendTaskMessage: botId (messageType 'bot'), починен путь isBotToken без req.user - authenticateTokenOrApiKey: JWT или X-Api-Key с белым списком endpoint'ов (/api/upload, messages, field-values, create task), проверки скоупов, аудит source='api' - Лимиты файлов через env: UPLOAD_IMAGE_MAX_MB=25, UPLOAD_DOC_MAX_MB=100, UPLOAD_MAX_MB=100 - MCP upload-инструменты: параметр fileUrl для привязки уже загруженного файла; новый инструмент get_api_guide
This commit is contained in:
@@ -23,7 +23,8 @@ type MessageAttachment = { url: string; name: string; size: number; mimeType?: s
|
||||
|
||||
export interface SendTaskMessageParams {
|
||||
task: Task; // задача (уже загружена и проверена вызывающим кодом)
|
||||
user: User; // автор сообщения
|
||||
user?: User; // автор сообщения; необязателен, если передан botId
|
||||
botId?: number | null; // сообщение от бота: authorId=null, botId, messageType='bot'
|
||||
organizationId: number;
|
||||
message: string;
|
||||
messageType?: string; // 'comment' (default), 'bot', 'system', ...
|
||||
@@ -41,8 +42,14 @@ export interface SendTaskMessageParams {
|
||||
// постановка embedding в очередь, запись взаимодействия с задачей.
|
||||
// Логика вынесена из POST /api/tasks/:id/messages (routes/chat.messages.routes.ts)
|
||||
// и переиспользуется MCP-сервером — поведение не менять.
|
||||
// При botId (сообщение от бота) авторство и пользовательские side-эффекты пропускаются,
|
||||
// как в POST /api/bot/message (bot-api.routes.ts).
|
||||
export async function sendTaskMessage(params: SendTaskMessageParams): Promise<TaskMessage> {
|
||||
const { task, user, organizationId } = params;
|
||||
const botId = params.botId ?? null;
|
||||
if (!user && !botId) {
|
||||
throw new SendTaskMessageError('Не указан автор сообщения (user или botId)', 500);
|
||||
}
|
||||
const taskId = task.id;
|
||||
|
||||
let replyToMessage = null;
|
||||
@@ -59,7 +66,7 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
|
||||
const orgUsers = await storage.getUsersByOrganization(organizationId);
|
||||
const orgUserIds = orgUsers.map(u => u.id);
|
||||
mentionedUserIds = params.mentionedUserIds.filter(id =>
|
||||
orgUserIds.includes(id) && id !== user.id
|
||||
orgUserIds.includes(id) && id !== user?.id
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,15 +74,16 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
|
||||
// 1. messageType is 'bot' or 'system' (messages authored by the bot service)
|
||||
// 2. authorId is null in the payload (another indicator of a bot/system message)
|
||||
// 3. Request was authenticated via a bot-service JWT (isBotToken = true)
|
||||
const messageType = params.messageType || (botId ? 'bot' : 'comment');
|
||||
const isBotOrSystemMessage =
|
||||
params.messageType === 'bot' ||
|
||||
params.messageType === 'system' ||
|
||||
messageType === 'bot' ||
|
||||
messageType === 'system' ||
|
||||
params.bodyAuthorId === null ||
|
||||
params.isBotToken === true;
|
||||
|
||||
let mentionedBotIds: number[] = [];
|
||||
let mentionedBotsMap = new Map<number, Bot>();
|
||||
if (!isBotOrSystemMessage && params.mentionedBotIds && params.mentionedBotIds.length > 0) {
|
||||
if (!isBotOrSystemMessage && user && params.mentionedBotIds && params.mentionedBotIds.length > 0) {
|
||||
const orgBots = await storage.getBotsByOrganization(organizationId);
|
||||
const activeBots = orgBots.filter(b => b.isActive);
|
||||
activeBots.forEach(bot => mentionedBotsMap.set(bot.id, bot));
|
||||
@@ -86,10 +94,11 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
|
||||
const messageData = {
|
||||
taskId,
|
||||
formId: task.formId,
|
||||
authorId: user.id,
|
||||
authorId: botId ? null : (user?.id ?? null),
|
||||
botId,
|
||||
replyToMessageId: params.replyToMessageId || null,
|
||||
message: params.message || '',
|
||||
messageType: params.messageType || 'comment',
|
||||
messageType,
|
||||
mentionedUserIds: mentionedUserIds.length > 0 ? mentionedUserIds : null,
|
||||
attachments: params.attachments?.length ? params.attachments : null,
|
||||
};
|
||||
@@ -127,7 +136,7 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionedBotIds.length > 0) {
|
||||
if (mentionedBotIds.length > 0 && user) {
|
||||
const taskFieldValues = await storage.getTaskFieldValues(taskId, organizationId);
|
||||
const form = await storage.getForm(task.formId, organizationId);
|
||||
const taskWithFields = { ...task, fieldValues: taskFieldValues };
|
||||
@@ -226,40 +235,44 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
|
||||
taskId: taskId
|
||||
});
|
||||
|
||||
const notificationEvent: NotificationEvent = {
|
||||
type: replyToMessage ? EVENT_TYPES.TASK_COMMENT_REPLIED : EVENT_TYPES.TASK_COMMENT_CREATED,
|
||||
organizationId,
|
||||
triggeredBy: user.id,
|
||||
taskId: taskId,
|
||||
formId: task.formId,
|
||||
messageId: createdMessage.id,
|
||||
payload: {
|
||||
message: createdMessage.message.length > 100
|
||||
? createdMessage.message.substring(0, 100) + '...'
|
||||
: createdMessage.message,
|
||||
taskTitle: task.title,
|
||||
authorName: `${user.firstName || ''} ${user.middleName || ''} ${user.lastName || ''}`.trim(),
|
||||
originalAuthorId: replyToMessage?.authorId,
|
||||
},
|
||||
mentionedUserIds: mentionedUserIds,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
// Уведомления и исходящие вебхуки — только для сообщений от пользователя
|
||||
// (для bot-сообщений — как в POST /api/bot/message: без notificationService).
|
||||
if (user) {
|
||||
const notificationEvent: NotificationEvent = {
|
||||
type: replyToMessage ? EVENT_TYPES.TASK_COMMENT_REPLIED : EVENT_TYPES.TASK_COMMENT_CREATED,
|
||||
organizationId,
|
||||
triggeredBy: user.id,
|
||||
taskId: taskId,
|
||||
formId: task.formId,
|
||||
messageId: createdMessage.id,
|
||||
payload: {
|
||||
message: createdMessage.message.length > 100
|
||||
? createdMessage.message.substring(0, 100) + '...'
|
||||
: createdMessage.message,
|
||||
taskTitle: task.title,
|
||||
authorName: `${user.firstName || ''} ${user.middleName || ''} ${user.lastName || ''}`.trim(),
|
||||
originalAuthorId: replyToMessage?.authorId,
|
||||
},
|
||||
mentionedUserIds: mentionedUserIds,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
notificationService.processEvent(notificationEvent).then(notifiedUserIds => {
|
||||
notifiedUserIds.forEach(userId => {
|
||||
eventBus.publishEvent({
|
||||
type: 'notification',
|
||||
data: { type: notificationEvent.type, taskId, messageId: createdMessage.id },
|
||||
organizationId,
|
||||
userId: userId
|
||||
notificationService.processEvent(notificationEvent).then(notifiedUserIds => {
|
||||
notifiedUserIds.forEach(userId => {
|
||||
eventBus.publishEvent({
|
||||
type: 'notification',
|
||||
data: { type: notificationEvent.type, taskId, messageId: createdMessage.id },
|
||||
organizationId,
|
||||
userId: userId
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch(err => console.error('Notification processing error:', err));
|
||||
}).catch(err => console.error('Notification processing error:', err));
|
||||
|
||||
if (messageData.messageType === 'comment') {
|
||||
webhookService.dispatchComment(
|
||||
organizationId, taskId, task.formId, createdMessage, user.id
|
||||
).catch(err => console.error('Webhook dispatch error:', err));
|
||||
if (messageData.messageType === 'comment') {
|
||||
webhookService.dispatchComment(
|
||||
organizationId, taskId, task.formId, createdMessage, user.id
|
||||
).catch(err => console.error('Webhook dispatch error:', err));
|
||||
}
|
||||
}
|
||||
|
||||
if (messageData.messageType === 'comment') {
|
||||
@@ -267,7 +280,7 @@ export async function sendTaskMessage(params: SendTaskMessageParams): Promise<Ta
|
||||
.catch(err => console.error('[RAG] enqueue message embedding error:', err));
|
||||
}
|
||||
|
||||
if (user.id) {
|
||||
if (user?.id) {
|
||||
storage.recordTaskInteractionAuto(taskId, user.id, organizationId)
|
||||
.catch(err => console.error('recordTaskInteraction error:', err));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user