@ihazz/bitrix24 1.1.13 → 1.1.14
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/src/channel.d.ts +2 -0
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +1154 -143
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.d.ts +1 -0
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +11 -0
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/inbound-handler.d.ts +10 -0
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/dist/src/inbound-handler.js +196 -9
- package/dist/src/inbound-handler.js.map +1 -1
- package/dist/src/media-service.d.ts +2 -0
- package/dist/src/media-service.d.ts.map +1 -1
- package/dist/src/media-service.js +30 -0
- package/dist/src/media-service.js.map +1 -1
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/message-utils.js +40 -1
- package/dist/src/message-utils.js.map +1 -1
- package/dist/src/send-service.d.ts +1 -1
- package/dist/src/send-service.d.ts.map +1 -1
- package/dist/src/send-service.js +8 -2
- package/dist/src/send-service.js.map +1 -1
- package/package.json +1 -1
- package/src/channel.ts +1447 -202
- package/src/i18n.ts +13 -0
- package/src/inbound-handler.ts +247 -10
- package/src/media-service.ts +39 -0
- package/src/message-utils.ts +54 -1
- package/src/send-service.ts +12 -2
package/dist/src/channel.js
CHANGED
|
@@ -14,7 +14,7 @@ import { DEFAULT_AVATAR_BASE64 } from './bot-avatar.js';
|
|
|
14
14
|
import { Bitrix24ApiError, createVerboseLogger, defaultLogger, CHANNEL_PREFIX_RE } from './utils.js';
|
|
15
15
|
import { getBitrix24Runtime } from './runtime.js';
|
|
16
16
|
import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply, getCommandRegistrationPayload, } from './commands.js';
|
|
17
|
-
import { accessApproved, accessDenied, commandKeyboardLabels, groupPairingPending, mediaDownloadFailed, groupChatUnsupported, newSessionReplyTexts, onboardingDisclaimerMessage, onboardingMessage, normalizeNewSessionReply, ownerAndAllowedUsersOnly, personalBotOwnerOnly, welcomeKeyboardLabels, watchOwnerDmNotice, } from './i18n.js';
|
|
17
|
+
import { accessApproved, accessDenied, commandKeyboardLabels, emptyReplyFallback, groupPairingPending, mediaDownloadFailed, groupChatUnsupported, newSessionReplyTexts, onboardingDisclaimerMessage, onboardingMessage, normalizeNewSessionReply, ownerAndAllowedUsersOnly, personalBotOwnerOnly, welcomeKeyboardLabels, watchOwnerDmNotice, } from './i18n.js';
|
|
18
18
|
import { HistoryCache } from './history-cache.js';
|
|
19
19
|
import { markdownToBbCode } from './message-utils.js';
|
|
20
20
|
const PHASE_STATUS_DURATION_SECONDS = 8;
|
|
@@ -24,6 +24,7 @@ const THINKING_STATUS_REFRESH_GRACE_MS = 6000;
|
|
|
24
24
|
const DIRECT_TEXT_COALESCE_DEBOUNCE_MS = 200;
|
|
25
25
|
const DIRECT_TEXT_COALESCE_MAX_WAIT_MS = 5000;
|
|
26
26
|
const REPLY_FALLBACK_GRACE_MS = 750;
|
|
27
|
+
const EMPTY_REPLY_FALLBACK_GRACE_MS = 15 * 1000;
|
|
27
28
|
const ACCESS_DENIED_NOTICE_COOLDOWN_MS = 60000;
|
|
28
29
|
const AUTO_BOT_CODE_MAX_CANDIDATES = 100;
|
|
29
30
|
const MEDIA_DOWNLOAD_CONCURRENCY = 2;
|
|
@@ -40,11 +41,23 @@ const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
|
|
|
40
41
|
const ACTIVE_SESSION_NAMESPACE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
41
42
|
const NATIVE_FORWARD_ACK_TTL_MS = 60 * 1000;
|
|
42
43
|
const NATIVE_REACTION_ACK_TTL_MS = 60 * 1000;
|
|
44
|
+
const NATIVE_FORWARD_SUCCESS_REACTION = 'whiteHeavyCheckMark';
|
|
45
|
+
const REACTION_TYPING_RESET_DURATION_SECONDS = 1;
|
|
46
|
+
const GENERIC_OUTBOUND_FILE_NAME_RE = /^(translated?|translation|output|result|file|document)(?:[_-][a-z]{2,8})?$/i;
|
|
47
|
+
const GENERIC_INBOUND_FILE_NAME_RE = /^(?:file[_-]\d+|[0-9a-f]{8,}(?:-[0-9a-f]{4,})*(?:_file[_-]?\d+)?)$/i;
|
|
48
|
+
const RECENT_MEDIA_DELIVERY_TTL_MS = 60 * 1000;
|
|
49
|
+
const RECENT_SUCCESSFUL_ACTION_TTL_MS = 60 * 1000;
|
|
50
|
+
const RECENT_OUTBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
51
|
+
const RECENT_WATCH_NOTIFICATION_TTL_MS = 30 * 60 * 1000;
|
|
43
52
|
const RECENT_INBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
44
53
|
const RECENT_INBOUND_MESSAGE_LIMIT = 20;
|
|
45
54
|
const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
|
|
46
55
|
const pendingNativeForwardAcks = new Map();
|
|
47
56
|
const pendingNativeReactionAcks = new Map();
|
|
57
|
+
const recentSuccessfulMediaDeliveries = new Map();
|
|
58
|
+
const recentSuccessfulActionsByMessage = new Map();
|
|
59
|
+
const recentOutboundMessagesById = new Map();
|
|
60
|
+
const recentWatchNotificationsByMessage = new Map();
|
|
48
61
|
const recentInboundMessagesByDialog = new Map();
|
|
49
62
|
const inboundMessageContextById = new Map();
|
|
50
63
|
// ─── Emoji → B24 reaction code mapping ──────────────────────────────────
|
|
@@ -185,9 +198,324 @@ function resolvePendingNativeForwardAckDialogId(currentMessageId, fallbackDialog
|
|
|
185
198
|
}
|
|
186
199
|
return inboundMessageContextById.get(messageKey)?.dialogId ?? fallbackDialogId;
|
|
187
200
|
}
|
|
201
|
+
function resolveRecentInboundDialogId(currentMessageId, accountId) {
|
|
202
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
203
|
+
if (!messageKey) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
pruneRecentInboundMessages();
|
|
207
|
+
return inboundMessageContextById.get(messageKey)?.dialogId;
|
|
208
|
+
}
|
|
209
|
+
function splitFileNameParts(fileName) {
|
|
210
|
+
const normalizedFileName = basename(fileName).trim();
|
|
211
|
+
const lastDotIndex = normalizedFileName.lastIndexOf('.');
|
|
212
|
+
if (lastDotIndex <= 0) {
|
|
213
|
+
return { stem: normalizedFileName, extension: '' };
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
stem: normalizedFileName.slice(0, lastDotIndex).trim(),
|
|
217
|
+
extension: normalizedFileName.slice(lastDotIndex).trim(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function normalizeSourceFileStem(stem) {
|
|
221
|
+
return stem
|
|
222
|
+
.trim()
|
|
223
|
+
.replace(/\s+\(\d+\)$/u, '')
|
|
224
|
+
.trim();
|
|
225
|
+
}
|
|
226
|
+
function isGenericOutboundFileName(fileName) {
|
|
227
|
+
const { stem } = splitFileNameParts(fileName);
|
|
228
|
+
return GENERIC_OUTBOUND_FILE_NAME_RE.test(stem);
|
|
229
|
+
}
|
|
230
|
+
function isSemanticInboundFileName(fileName) {
|
|
231
|
+
const { stem } = splitFileNameParts(fileName);
|
|
232
|
+
return Boolean(stem) && !GENERIC_INBOUND_FILE_NAME_RE.test(stem);
|
|
233
|
+
}
|
|
234
|
+
function inferTranslationSuffixFromBody(body) {
|
|
235
|
+
const normalizedBody = body.toLowerCase();
|
|
236
|
+
if (!/translate|translation|перевед|перевод/u.test(normalizedBody)) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const languagePatterns = [
|
|
240
|
+
['en', /english|английск/u],
|
|
241
|
+
['ru', /russian|русск/u],
|
|
242
|
+
['de', /german|немец/u],
|
|
243
|
+
['fr', /french|француз/u],
|
|
244
|
+
['es', /spanish|испан/u],
|
|
245
|
+
['it', /italian|итальян/u],
|
|
246
|
+
['pt', /portuguese|португал/u],
|
|
247
|
+
['zh', /chinese|китай/u],
|
|
248
|
+
['ja', /japanese|япон/u],
|
|
249
|
+
['ko', /korean|корей/u],
|
|
250
|
+
];
|
|
251
|
+
for (const [code, pattern] of languagePatterns) {
|
|
252
|
+
if (pattern.test(normalizedBody)) {
|
|
253
|
+
return `_${code}`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return '_translated';
|
|
257
|
+
}
|
|
258
|
+
function inferTranslationSuffix(requestedFileName, body) {
|
|
259
|
+
const { stem } = splitFileNameParts(requestedFileName);
|
|
260
|
+
const suffixMatch = stem.match(/(?:^|[_-])(en|ru|de|fr|es|it|pt|zh|ja|ko)$/i);
|
|
261
|
+
if (suffixMatch?.[1]) {
|
|
262
|
+
return `_${suffixMatch[1].toLowerCase()}`;
|
|
263
|
+
}
|
|
264
|
+
return inferTranslationSuffixFromBody(body);
|
|
265
|
+
}
|
|
266
|
+
function resolveRecentInboundContext(params) {
|
|
267
|
+
pruneRecentInboundMessages();
|
|
268
|
+
const explicitKey = buildAccountMessageScopeKey(params.accountId, params.currentMessageId);
|
|
269
|
+
const explicitEntry = explicitKey ? inboundMessageContextById.get(explicitKey) : undefined;
|
|
270
|
+
if (explicitEntry) {
|
|
271
|
+
return explicitEntry;
|
|
272
|
+
}
|
|
273
|
+
const dialogKey = buildAccountDialogScopeKey(params.accountId, params.dialogId);
|
|
274
|
+
if (!dialogKey) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
const recentEntries = recentInboundMessagesByDialog.get(dialogKey);
|
|
278
|
+
if (!recentEntries?.length) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
for (let index = recentEntries.length - 1; index >= 0; index -= 1) {
|
|
282
|
+
const entry = recentEntries[index];
|
|
283
|
+
const messageKey = buildAccountMessageScopeKey(params.accountId, entry.messageId);
|
|
284
|
+
if (!messageKey) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const inboundEntry = inboundMessageContextById.get(messageKey);
|
|
288
|
+
if (inboundEntry) {
|
|
289
|
+
return inboundEntry;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
function resolveSemanticOutboundFileName(params) {
|
|
295
|
+
const requestedBaseName = basename(params.requestedFileName).trim();
|
|
296
|
+
if (!requestedBaseName || !isGenericOutboundFileName(requestedBaseName)) {
|
|
297
|
+
return requestedBaseName;
|
|
298
|
+
}
|
|
299
|
+
const inboundContext = resolveRecentInboundContext({
|
|
300
|
+
dialogId: params.dialogId,
|
|
301
|
+
currentMessageId: params.currentMessageId,
|
|
302
|
+
accountId: params.accountId,
|
|
303
|
+
});
|
|
304
|
+
if (!inboundContext || inboundContext.mediaNames.length !== 1) {
|
|
305
|
+
return requestedBaseName;
|
|
306
|
+
}
|
|
307
|
+
const sourceFileName = inboundContext.mediaNames[0];
|
|
308
|
+
if (!isSemanticInboundFileName(sourceFileName)) {
|
|
309
|
+
return requestedBaseName;
|
|
310
|
+
}
|
|
311
|
+
const { stem: sourceStemRaw, extension: sourceExtension } = splitFileNameParts(sourceFileName);
|
|
312
|
+
const { extension: requestedExtension } = splitFileNameParts(requestedBaseName);
|
|
313
|
+
const normalizedSourceStem = normalizeSourceFileStem(sourceStemRaw);
|
|
314
|
+
if (!normalizedSourceStem) {
|
|
315
|
+
return requestedBaseName;
|
|
316
|
+
}
|
|
317
|
+
const suffix = inferTranslationSuffix(requestedBaseName, inboundContext.body) ?? '';
|
|
318
|
+
const nextStem = suffix && !normalizedSourceStem.endsWith(suffix)
|
|
319
|
+
? `${normalizedSourceStem}${suffix}`
|
|
320
|
+
: normalizedSourceStem;
|
|
321
|
+
const nextExtension = requestedExtension || sourceExtension;
|
|
322
|
+
return nextExtension ? `${nextStem}${nextExtension}` : nextStem;
|
|
323
|
+
}
|
|
324
|
+
async function maybeSendReactionTypingReset(params) {
|
|
325
|
+
const dialogId = typeof params.dialogId === 'string' ? params.dialogId.trim() : '';
|
|
326
|
+
if (!dialogId) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const sendTyping = params.sendService.sendTyping;
|
|
330
|
+
if (typeof sendTyping !== 'function') {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await sendTyping.call(params.sendService, { webhookUrl: params.webhookUrl, bot: params.bot, dialogId }, REACTION_TYPING_RESET_DURATION_SECONDS);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
defaultLogger.debug('Failed to send Bitrix24 reaction typing reset', {
|
|
338
|
+
dialogId,
|
|
339
|
+
error: error instanceof Error ? error.message : String(error),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function maybeAddNativeForwardSuccessReaction(params) {
|
|
344
|
+
const messageId = toMessageId(params.currentMessageId);
|
|
345
|
+
if (!messageId) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const addReaction = params.api.addReaction;
|
|
349
|
+
if (typeof addReaction !== 'function') {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let shouldResetTyping = false;
|
|
353
|
+
try {
|
|
354
|
+
await addReaction.call(params.api, params.webhookUrl, params.bot, messageId, NATIVE_FORWARD_SUCCESS_REACTION);
|
|
355
|
+
shouldResetTyping = true;
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
359
|
+
if (errorMessage.includes('REACTION_ALREADY_SET')) {
|
|
360
|
+
defaultLogger.debug('Native forward success reaction already set', { messageId });
|
|
361
|
+
shouldResetTyping = true;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
defaultLogger.warn('Failed to add native forward success reaction', {
|
|
365
|
+
messageId,
|
|
366
|
+
error: errorMessage,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (shouldResetTyping) {
|
|
371
|
+
await maybeSendReactionTypingReset({
|
|
372
|
+
sendService: params.sendService,
|
|
373
|
+
webhookUrl: params.webhookUrl,
|
|
374
|
+
bot: params.bot,
|
|
375
|
+
dialogId: params.dialogId,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
188
379
|
function buildPendingNativeReactionAckKey(currentMessageId, accountId) {
|
|
189
380
|
return buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
190
381
|
}
|
|
382
|
+
function pruneRecentSuccessfulActions(now = Date.now()) {
|
|
383
|
+
for (const [key, expiresAt] of recentSuccessfulActionsByMessage.entries()) {
|
|
384
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
385
|
+
recentSuccessfulActionsByMessage.delete(key);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function markRecentSuccessfulAction(currentMessageId, accountId) {
|
|
390
|
+
const key = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
391
|
+
if (!key) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
pruneRecentSuccessfulActions();
|
|
395
|
+
recentSuccessfulActionsByMessage.set(key, Date.now() + RECENT_SUCCESSFUL_ACTION_TTL_MS);
|
|
396
|
+
}
|
|
397
|
+
function hasRecentSuccessfulAction(currentMessageId, accountId) {
|
|
398
|
+
const key = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
399
|
+
if (!key) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
pruneRecentSuccessfulActions();
|
|
403
|
+
return recentSuccessfulActionsByMessage.has(key);
|
|
404
|
+
}
|
|
405
|
+
function pruneRecentOutboundMessages(now = Date.now()) {
|
|
406
|
+
for (const [key, expiresAt] of recentOutboundMessagesById.entries()) {
|
|
407
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
408
|
+
recentOutboundMessagesById.delete(key);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function markRecentOutboundMessage(messageId, accountId) {
|
|
413
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
414
|
+
if (!key) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
pruneRecentOutboundMessages();
|
|
418
|
+
recentOutboundMessagesById.set(key, Date.now() + RECENT_OUTBOUND_MESSAGE_TTL_MS);
|
|
419
|
+
}
|
|
420
|
+
function hasRecentOutboundMessage(messageId, accountId) {
|
|
421
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
422
|
+
if (!key) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
pruneRecentOutboundMessages();
|
|
426
|
+
return recentOutboundMessagesById.has(key);
|
|
427
|
+
}
|
|
428
|
+
function pruneRecentWatchNotifications(now = Date.now()) {
|
|
429
|
+
for (const [key, expiresAt] of recentWatchNotificationsByMessage.entries()) {
|
|
430
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
431
|
+
recentWatchNotificationsByMessage.delete(key);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function markRecentWatchNotification(messageId, accountId) {
|
|
436
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
437
|
+
if (!key) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
pruneRecentWatchNotifications();
|
|
441
|
+
recentWatchNotificationsByMessage.set(key, Date.now() + RECENT_WATCH_NOTIFICATION_TTL_MS);
|
|
442
|
+
}
|
|
443
|
+
function hasRecentWatchNotification(messageId, accountId) {
|
|
444
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
445
|
+
if (!key) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
pruneRecentWatchNotifications();
|
|
449
|
+
return recentWatchNotificationsByMessage.has(key);
|
|
450
|
+
}
|
|
451
|
+
function buildRecentMediaDeliveryKey(dialogId, currentMessageId, accountId) {
|
|
452
|
+
return buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
453
|
+
}
|
|
454
|
+
function normalizeMediaDeliveryName(mediaUrl) {
|
|
455
|
+
return basename(mediaUrl.trim()).toLowerCase();
|
|
456
|
+
}
|
|
457
|
+
function pruneRecentMediaDeliveries(now = Date.now()) {
|
|
458
|
+
for (const [key, entry] of recentSuccessfulMediaDeliveries.entries()) {
|
|
459
|
+
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
460
|
+
recentSuccessfulMediaDeliveries.delete(key);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function markRecentMediaDelivery(params) {
|
|
465
|
+
const key = buildRecentMediaDeliveryKey(params.dialogId, params.currentMessageId, params.accountId);
|
|
466
|
+
if (!key || params.mediaUrls.length === 0) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
pruneRecentMediaDeliveries();
|
|
470
|
+
const mediaByName = recentSuccessfulMediaDeliveries.get(key)?.mediaByName ?? new Map();
|
|
471
|
+
const normalizedMessageId = String(params.messageId ?? '').trim();
|
|
472
|
+
for (const mediaUrl of params.mediaUrls) {
|
|
473
|
+
const mediaName = normalizeMediaDeliveryName(mediaUrl);
|
|
474
|
+
if (!mediaName) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
mediaByName.set(mediaName, normalizedMessageId);
|
|
478
|
+
}
|
|
479
|
+
recentSuccessfulMediaDeliveries.set(key, {
|
|
480
|
+
expiresAt: Date.now() + RECENT_MEDIA_DELIVERY_TTL_MS,
|
|
481
|
+
mediaByName,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function findRecentMediaDelivery(params) {
|
|
485
|
+
const key = buildRecentMediaDeliveryKey(params.dialogId, params.currentMessageId, params.accountId);
|
|
486
|
+
if (!key || params.mediaUrls.length === 0) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
pruneRecentMediaDeliveries();
|
|
490
|
+
const entry = recentSuccessfulMediaDeliveries.get(key);
|
|
491
|
+
if (!entry) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const normalizedNames = params.mediaUrls
|
|
495
|
+
.map((mediaUrl) => normalizeMediaDeliveryName(mediaUrl))
|
|
496
|
+
.filter(Boolean);
|
|
497
|
+
if (normalizedNames.length === 0) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
for (const mediaName of normalizedNames) {
|
|
501
|
+
if (!entry.mediaByName.has(mediaName)) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const lastMessageId = normalizedNames
|
|
506
|
+
.map((mediaName) => entry.mediaByName.get(mediaName) ?? '')
|
|
507
|
+
.find(Boolean);
|
|
508
|
+
return lastMessageId ? { messageId: lastMessageId } : {};
|
|
509
|
+
}
|
|
510
|
+
function hasRecentMediaDelivery(dialogId, currentMessageId, accountId) {
|
|
511
|
+
const key = buildRecentMediaDeliveryKey(dialogId, currentMessageId, accountId);
|
|
512
|
+
if (!key) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
pruneRecentMediaDeliveries();
|
|
516
|
+
const entry = recentSuccessfulMediaDeliveries.get(key);
|
|
517
|
+
return Boolean(entry && entry.mediaByName.size > 0);
|
|
518
|
+
}
|
|
191
519
|
function prunePendingNativeReactionAcks(now = Date.now()) {
|
|
192
520
|
for (const [key, entry] of pendingNativeReactionAcks.entries()) {
|
|
193
521
|
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
@@ -239,7 +567,7 @@ function pruneRecentInboundMessages(now = Date.now()) {
|
|
|
239
567
|
}
|
|
240
568
|
}
|
|
241
569
|
}
|
|
242
|
-
function rememberRecentInboundMessage(dialogId, messageId, body, timestamp = Date.now(), accountId) {
|
|
570
|
+
function rememberRecentInboundMessage(dialogId, messageId, body, mediaNames = [], timestamp = Date.now(), accountId) {
|
|
243
571
|
const normalizedMessageId = toMessageId(messageId);
|
|
244
572
|
const normalizedBody = body.trim();
|
|
245
573
|
const dialogKey = buildAccountDialogScopeKey(accountId, dialogId);
|
|
@@ -259,6 +587,10 @@ function rememberRecentInboundMessage(dialogId, messageId, body, timestamp = Dat
|
|
|
259
587
|
dialogId,
|
|
260
588
|
dialogKey,
|
|
261
589
|
body: normalizedBody,
|
|
590
|
+
mediaNames: mediaNames
|
|
591
|
+
.filter((name) => typeof name === 'string')
|
|
592
|
+
.map((name) => name.trim())
|
|
593
|
+
.filter(Boolean),
|
|
262
594
|
timestamp,
|
|
263
595
|
});
|
|
264
596
|
}
|
|
@@ -381,6 +713,19 @@ function extractReplyDirective(params) {
|
|
|
381
713
|
}
|
|
382
714
|
return { cleanText };
|
|
383
715
|
}
|
|
716
|
+
function isBitrix24DirectDialogId(dialogId) {
|
|
717
|
+
const normalizedDialogId = String(dialogId ?? '').trim();
|
|
718
|
+
return normalizedDialogId.length > 0 && !normalizedDialogId.startsWith('chat');
|
|
719
|
+
}
|
|
720
|
+
function shouldUseBitrix24NativeReply(params) {
|
|
721
|
+
if (typeof params.isDm === 'boolean') {
|
|
722
|
+
return !params.isDm;
|
|
723
|
+
}
|
|
724
|
+
if (!params.dialogId) {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
return !isBitrix24DirectDialogId(params.dialogId);
|
|
728
|
+
}
|
|
384
729
|
function findNativeForwardTarget(params) {
|
|
385
730
|
const deliveredText = normalizeComparableMessageText(params.deliveredText);
|
|
386
731
|
if (!deliveredText) {
|
|
@@ -491,8 +836,8 @@ function buildBitrix24NativeReplyAgentHint(currentMessageId) {
|
|
|
491
836
|
return [
|
|
492
837
|
'[Bitrix24 native reply instruction]',
|
|
493
838
|
`Current Bitrix24 message id: ${normalizedMessageId}.`,
|
|
494
|
-
'
|
|
495
|
-
'For text-only payloads you can also prepend [[reply_to_current]] to the reply text.',
|
|
839
|
+
'Only in group chats, if you intentionally want a native Bitrix24 reply to the current inbound message, set "replyToMessageId" to that id.',
|
|
840
|
+
'For text-only payloads in group chats, you can also prepend [[reply_to_current]] to the reply text.',
|
|
496
841
|
'Do not use a native reply unless you actually want reply threading.',
|
|
497
842
|
'[/Bitrix24 native reply instruction]',
|
|
498
843
|
].join('\n');
|
|
@@ -500,13 +845,46 @@ function buildBitrix24NativeReplyAgentHint(currentMessageId) {
|
|
|
500
845
|
function buildBitrix24FileDeliveryAgentHint() {
|
|
501
846
|
return [
|
|
502
847
|
'[Bitrix24 file delivery instruction]',
|
|
503
|
-
'When you need to deliver a real file or document to the user,
|
|
848
|
+
'When you need to deliver a real file or document to the user, prefer a structured reply payload with field "mediaUrl" or "mediaUrls".',
|
|
504
849
|
'Set "mediaUrl" to the local path of the generated file in the OpenClaw workspace or managed media directory.',
|
|
850
|
+
'If the current prompt already includes a <file ...>...</file> block with the file contents, use that inline content directly and do not call read on the attached Bitrix24 media path unless you truly need to.',
|
|
851
|
+
'If read on an attached Bitrix24 media path fails, fall back to the inline <file> content from the current prompt instead of giving up.',
|
|
852
|
+
'If you use the generic message tool instead of a structured reply payload, attach the local file with "media", "filePath", or "path". Do not use "mediaUrl" with the message tool.',
|
|
505
853
|
'Use "text" only for an optional short caption.',
|
|
854
|
+
'For file requests, follow this order exactly: read/prepare content, write the output file, send that file once, then stop.',
|
|
855
|
+
'Never call the file-delivery tool before the write tool has succeeded for that file path.',
|
|
856
|
+
'If the upload tool says the media upload failed, fix the file path or create the file first. Do not blindly retry the same send.',
|
|
857
|
+
'After one successful file upload in a turn, do not send the same file again and do not send an extra text-only confirmation.',
|
|
858
|
+
'Do not output "NO_REPLY" unless you already called the message tool or emitted a structured reply payload with mediaUrl/mediaUrls in this same turn.',
|
|
859
|
+
'Do not output "NO_REPLY" after only read/write tools. NO_REPLY is allowed only after the actual file-delivery tool result succeeded in this same turn.',
|
|
860
|
+
'If the user asked for a file and you have not emitted the actual file delivery payload yet, do not answer with "NO_REPLY".',
|
|
506
861
|
'Do not place local file paths inside markdown links or plain text, because that only sends text to the user and does not upload the file.',
|
|
507
862
|
'[/Bitrix24 file delivery instruction]',
|
|
508
863
|
].join('\n');
|
|
509
864
|
}
|
|
865
|
+
export function buildBitrix24InlineButtonsAgentHint() {
|
|
866
|
+
return [
|
|
867
|
+
'[Bitrix24 buttons instruction]',
|
|
868
|
+
'For an ordinary reply in the current Bitrix24 dialog that needs buttons or multiple choices, prefer normal assistant text with inline button markup instead of calling the message tool.',
|
|
869
|
+
'Append the buttons to the end of the visible reply text using the exact format [[{"text":"Option 1","callback_data":"one"},{"text":"Option 2","callback_data":"two"}]].',
|
|
870
|
+
'The channel will strip that markup from the visible text and render native Bitrix24 buttons automatically.',
|
|
871
|
+
'Use the message tool for Bitrix24 only when you truly need an explicit action such as send to another dialog, native forward/reply, reaction, edit, delete, or file upload.',
|
|
872
|
+
'[/Bitrix24 buttons instruction]',
|
|
873
|
+
].join('\n');
|
|
874
|
+
}
|
|
875
|
+
function getDispatchCount(counts, key) {
|
|
876
|
+
const value = counts?.[key];
|
|
877
|
+
return Number.isFinite(value) ? Number(value) : 0;
|
|
878
|
+
}
|
|
879
|
+
function isEmptyDispatchResult(dispatchResult) {
|
|
880
|
+
if (!dispatchResult || dispatchResult.queuedFinal !== false) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
const toolCount = getDispatchCount(dispatchResult.counts, 'tool');
|
|
884
|
+
const blockCount = getDispatchCount(dispatchResult.counts, 'block');
|
|
885
|
+
const finalCount = getDispatchCount(dispatchResult.counts, 'final');
|
|
886
|
+
return toolCount + blockCount + finalCount === 0;
|
|
887
|
+
}
|
|
510
888
|
function readExplicitForwardMessageIds(rawParams) {
|
|
511
889
|
return parseActionMessageIds(rawParams.forwardMessageIds
|
|
512
890
|
?? rawParams.forwardIds);
|
|
@@ -689,6 +1067,9 @@ function createReplyStatusHeartbeat(params) {
|
|
|
689
1067
|
async function waitReplyFallbackGraceWindow() {
|
|
690
1068
|
await new Promise((resolve) => setTimeout(resolve, REPLY_FALLBACK_GRACE_MS));
|
|
691
1069
|
}
|
|
1070
|
+
async function waitEmptyReplyFallbackGraceWindow() {
|
|
1071
|
+
await new Promise((resolve) => setTimeout(resolve, EMPTY_REPLY_FALLBACK_GRACE_MS));
|
|
1072
|
+
}
|
|
692
1073
|
export function canCoalesceDirectMessage(msgCtx, config) {
|
|
693
1074
|
return msgCtx.isDm
|
|
694
1075
|
&& config.dmPolicy !== 'pairing'
|
|
@@ -1171,12 +1552,16 @@ export function __setGatewayStateForTests(state) {
|
|
|
1171
1552
|
if (state === null) {
|
|
1172
1553
|
pendingNativeForwardAcks.clear();
|
|
1173
1554
|
pendingNativeReactionAcks.clear();
|
|
1555
|
+
recentSuccessfulMediaDeliveries.clear();
|
|
1556
|
+
recentSuccessfulActionsByMessage.clear();
|
|
1557
|
+
recentOutboundMessagesById.clear();
|
|
1558
|
+
recentWatchNotificationsByMessage.clear();
|
|
1174
1559
|
recentInboundMessagesByDialog.clear();
|
|
1175
1560
|
inboundMessageContextById.clear();
|
|
1176
1561
|
}
|
|
1177
1562
|
}
|
|
1178
1563
|
export function __rememberRecentInboundMessageForTests(params) {
|
|
1179
|
-
rememberRecentInboundMessage(params.dialogId, params.messageId, params.body, params.timestamp, params.accountId);
|
|
1564
|
+
rememberRecentInboundMessage(params.dialogId, params.messageId, params.body, params.mediaNames, params.timestamp, params.accountId);
|
|
1180
1565
|
}
|
|
1181
1566
|
// ─── Keyboard layouts ────────────────────────────────────────────────────────
|
|
1182
1567
|
export function buildWelcomeKeyboard(language) {
|
|
@@ -1209,83 +1594,24 @@ const BITRIX24_DISCOVERY_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'dele
|
|
|
1209
1594
|
function buildMessageToolButtonsSchema() {
|
|
1210
1595
|
return {
|
|
1211
1596
|
type: 'array',
|
|
1212
|
-
description: 'Optional Bitrix24 message buttons as rows of button objects.',
|
|
1597
|
+
description: 'Optional Bitrix24 message buttons as rows of button objects. Each button object can include text, optional callback_data and optional style.',
|
|
1213
1598
|
items: {
|
|
1214
1599
|
type: 'array',
|
|
1215
1600
|
items: {
|
|
1216
1601
|
type: 'object',
|
|
1217
|
-
|
|
1218
|
-
properties: {
|
|
1219
|
-
text: {
|
|
1220
|
-
type: 'string',
|
|
1221
|
-
description: 'Visible button label.',
|
|
1222
|
-
},
|
|
1223
|
-
callback_data: {
|
|
1224
|
-
type: 'string',
|
|
1225
|
-
description: 'Registered command like /help, or any plain text payload to send when tapped.',
|
|
1226
|
-
},
|
|
1227
|
-
style: {
|
|
1228
|
-
type: 'string',
|
|
1229
|
-
enum: ['primary', 'attention', 'danger'],
|
|
1230
|
-
description: 'Optional Bitrix24 button accent.',
|
|
1231
|
-
},
|
|
1232
|
-
},
|
|
1233
|
-
required: ['text'],
|
|
1602
|
+
description: 'Single button object. Use text, optional callback_data, and optional style=primary|attention|danger.',
|
|
1234
1603
|
},
|
|
1235
1604
|
},
|
|
1236
1605
|
};
|
|
1237
1606
|
}
|
|
1238
1607
|
function buildMessageToolAttachSchema() {
|
|
1239
|
-
const blockSchema = {
|
|
1240
|
-
type: 'object',
|
|
1241
|
-
description: 'Single Bitrix24 ATTACH rich block. Supported top-level keys are MESSAGE, LINK, IMAGE, FILE, DELIMITER, GRID or USER.',
|
|
1242
|
-
};
|
|
1243
1608
|
return {
|
|
1244
|
-
description: 'Bitrix24 ATTACH rich layout blocks. Use this for rich cards, not for binary file uploads.
|
|
1245
|
-
anyOf: [
|
|
1246
|
-
{
|
|
1247
|
-
type: 'object',
|
|
1248
|
-
additionalProperties: false,
|
|
1249
|
-
properties: {
|
|
1250
|
-
ID: { type: 'integer' },
|
|
1251
|
-
COLOR_TOKEN: {
|
|
1252
|
-
type: 'string',
|
|
1253
|
-
enum: ['primary', 'secondary', 'alert', 'base'],
|
|
1254
|
-
},
|
|
1255
|
-
COLOR: { type: 'string' },
|
|
1256
|
-
BLOCKS: {
|
|
1257
|
-
type: 'array',
|
|
1258
|
-
items: blockSchema,
|
|
1259
|
-
},
|
|
1260
|
-
},
|
|
1261
|
-
required: ['BLOCKS'],
|
|
1262
|
-
},
|
|
1263
|
-
{
|
|
1264
|
-
type: 'array',
|
|
1265
|
-
items: blockSchema,
|
|
1266
|
-
},
|
|
1267
|
-
],
|
|
1609
|
+
description: 'Bitrix24 ATTACH rich layout blocks. Use this for rich cards, not for binary file uploads. Accepts either an array of block objects or an object with BLOCKS array. Prefer direct Bitrix block keys such as [{USER:{USER_ID:1,NAME:"Eugene"}},{GRID:[{DISPLAY:"LINE",NAME:"Status",VALUE:"In progress"},{DISPLAY:"LINE",NAME:"Owner",VALUE:"Eugene"}]},{LINK:{NAME:"Open",LINK:"https://example.com"}}]. Supported top-level block keys include MESSAGE, LINK, IMAGE, FILE, DELIMITER, GRID and USER. For actual file uploads, use the generic message tool fields media/filePath/path or a structured reply payload with mediaUrl/mediaUrls.',
|
|
1268
1610
|
};
|
|
1269
1611
|
}
|
|
1270
1612
|
function buildMessageToolIdListSchema(description) {
|
|
1271
1613
|
return {
|
|
1272
|
-
|
|
1273
|
-
{
|
|
1274
|
-
type: 'integer',
|
|
1275
|
-
description,
|
|
1276
|
-
},
|
|
1277
|
-
{
|
|
1278
|
-
type: 'string',
|
|
1279
|
-
description: `${description} Can also be a comma-separated list or JSON array string.`,
|
|
1280
|
-
},
|
|
1281
|
-
{
|
|
1282
|
-
type: 'array',
|
|
1283
|
-
description,
|
|
1284
|
-
items: {
|
|
1285
|
-
type: 'integer',
|
|
1286
|
-
},
|
|
1287
|
-
},
|
|
1288
|
-
],
|
|
1614
|
+
description: `${description} Can be an integer, a string, or an array of integers.`,
|
|
1289
1615
|
};
|
|
1290
1616
|
}
|
|
1291
1617
|
function resolveConfiguredBitrix24ActionAccounts(cfg, accountId) {
|
|
@@ -1310,22 +1636,16 @@ function describeBitrix24MessageTool(params) {
|
|
|
1310
1636
|
return {
|
|
1311
1637
|
actions: [...BITRIX24_DISCOVERY_ACTION_NAMES],
|
|
1312
1638
|
capabilities: ['interactive', 'buttons', 'cards'],
|
|
1313
|
-
schema:
|
|
1314
|
-
{
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
attach: buildMessageToolAttachSchema(),
|
|
1322
|
-
forwardMessageIds: buildMessageToolIdListSchema('Bitrix24 source message id(s) to forward natively. Use with action="send" plus to/target. Prefer the referenced message id, not the current instruction message id. The message field must be non-empty — use "↩️" (emoji) for a pure forward without extra text.'),
|
|
1323
|
-
forwardIds: buildMessageToolIdListSchema('Alias for forwardMessageIds. Use with action="send".'),
|
|
1324
|
-
replyToId: buildMessageToolIdListSchema('Bitrix24 message id to reply to natively inside the current dialog.'),
|
|
1325
|
-
replyToMessageId: buildMessageToolIdListSchema('Alias for replyToId.'),
|
|
1326
|
-
},
|
|
1639
|
+
schema: {
|
|
1640
|
+
properties: {
|
|
1641
|
+
buttons: buildMessageToolButtonsSchema(),
|
|
1642
|
+
attach: buildMessageToolAttachSchema(),
|
|
1643
|
+
forwardMessageIds: buildMessageToolIdListSchema('Bitrix24 source message id(s) to forward natively. Use with action="send" plus to/target. Prefer the referenced message id, not the current instruction message id. The message field must be non-empty — use "↩️" (emoji) for a pure forward without extra text.'),
|
|
1644
|
+
forwardIds: buildMessageToolIdListSchema('Alias for forwardMessageIds. Use with action="send".'),
|
|
1645
|
+
replyToId: buildMessageToolIdListSchema('Bitrix24 message id to reply to natively inside the current dialog.'),
|
|
1646
|
+
replyToMessageId: buildMessageToolIdListSchema('Alias for replyToId.'),
|
|
1327
1647
|
},
|
|
1328
|
-
|
|
1648
|
+
},
|
|
1329
1649
|
};
|
|
1330
1650
|
}
|
|
1331
1651
|
function extractBitrix24ToolSend(args) {
|
|
@@ -1363,6 +1683,9 @@ function isAttachColorToken(value) {
|
|
|
1363
1683
|
|| value === 'alert'
|
|
1364
1684
|
|| value === 'base';
|
|
1365
1685
|
}
|
|
1686
|
+
function isPresent(value) {
|
|
1687
|
+
return value != null;
|
|
1688
|
+
}
|
|
1366
1689
|
function isAttachBlock(value) {
|
|
1367
1690
|
return isPlainObject(value) && Object.keys(value).length > 0;
|
|
1368
1691
|
}
|
|
@@ -1378,6 +1701,362 @@ function isBitrix24Attach(value) {
|
|
|
1378
1701
|
}
|
|
1379
1702
|
return value.BLOCKS.every(isAttachBlock);
|
|
1380
1703
|
}
|
|
1704
|
+
function readAttachString(value) {
|
|
1705
|
+
if (typeof value !== 'string') {
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
const trimmed = value.trim();
|
|
1709
|
+
return trimmed ? trimmed : undefined;
|
|
1710
|
+
}
|
|
1711
|
+
function readAttachNumber(value) {
|
|
1712
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1713
|
+
return Math.trunc(value);
|
|
1714
|
+
}
|
|
1715
|
+
if (typeof value === 'string') {
|
|
1716
|
+
const trimmed = value.trim();
|
|
1717
|
+
if (!trimmed) {
|
|
1718
|
+
return undefined;
|
|
1719
|
+
}
|
|
1720
|
+
const normalized = Number(trimmed);
|
|
1721
|
+
if (Number.isFinite(normalized)) {
|
|
1722
|
+
return Math.trunc(normalized);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return undefined;
|
|
1726
|
+
}
|
|
1727
|
+
function normalizeAttachColorTokenValue(value) {
|
|
1728
|
+
if (value === undefined) {
|
|
1729
|
+
return undefined;
|
|
1730
|
+
}
|
|
1731
|
+
if (typeof value !== 'string') {
|
|
1732
|
+
return undefined;
|
|
1733
|
+
}
|
|
1734
|
+
const normalized = value.trim().toLowerCase();
|
|
1735
|
+
return isAttachColorToken(normalized) ? normalized : undefined;
|
|
1736
|
+
}
|
|
1737
|
+
function normalizeAttachGridDisplayValue(value) {
|
|
1738
|
+
if (typeof value !== 'string') {
|
|
1739
|
+
return undefined;
|
|
1740
|
+
}
|
|
1741
|
+
const normalized = value.trim().toUpperCase();
|
|
1742
|
+
return normalized === 'BLOCK'
|
|
1743
|
+
|| normalized === 'LINE'
|
|
1744
|
+
|| normalized === 'ROW'
|
|
1745
|
+
|| normalized === 'TABLE'
|
|
1746
|
+
? normalized
|
|
1747
|
+
: undefined;
|
|
1748
|
+
}
|
|
1749
|
+
function normalizeAttachGridItem(rawValue) {
|
|
1750
|
+
if (!isPlainObject(rawValue)) {
|
|
1751
|
+
return undefined;
|
|
1752
|
+
}
|
|
1753
|
+
const display = normalizeAttachGridDisplayValue(rawValue.DISPLAY) ?? 'LINE';
|
|
1754
|
+
const name = readAttachString(rawValue.NAME);
|
|
1755
|
+
const value = readAttachString(rawValue.VALUE);
|
|
1756
|
+
const width = readAttachNumber(rawValue.WIDTH);
|
|
1757
|
+
const height = readAttachNumber(rawValue.HEIGHT);
|
|
1758
|
+
const colorToken = normalizeAttachColorTokenValue(rawValue.COLOR_TOKEN);
|
|
1759
|
+
const color = readAttachString(rawValue.COLOR);
|
|
1760
|
+
const link = readAttachString(rawValue.LINK);
|
|
1761
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
1762
|
+
const chatId = readAttachNumber(rawValue.CHAT_ID);
|
|
1763
|
+
if (!name && !value && !link && userId === undefined && chatId === undefined) {
|
|
1764
|
+
return undefined;
|
|
1765
|
+
}
|
|
1766
|
+
return {
|
|
1767
|
+
DISPLAY: display,
|
|
1768
|
+
...(name ? { NAME: name } : {}),
|
|
1769
|
+
...(value ? { VALUE: value } : {}),
|
|
1770
|
+
...(width !== undefined ? { WIDTH: width } : {}),
|
|
1771
|
+
...(height !== undefined ? { HEIGHT: height } : {}),
|
|
1772
|
+
...(colorToken ? { COLOR_TOKEN: colorToken } : {}),
|
|
1773
|
+
...(color ? { COLOR: color } : {}),
|
|
1774
|
+
...(link ? { LINK: link } : {}),
|
|
1775
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
1776
|
+
...(chatId !== undefined ? { CHAT_ID: chatId } : {}),
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
function normalizeAttachLinkValue(rawValue, aliasSource) {
|
|
1780
|
+
if (typeof rawValue === 'string') {
|
|
1781
|
+
const link = readAttachString(rawValue);
|
|
1782
|
+
if (!link) {
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
const aliasName = aliasSource
|
|
1786
|
+
? readAttachString(aliasSource.NAME) ?? readAttachString(aliasSource.TITLE)
|
|
1787
|
+
: undefined;
|
|
1788
|
+
const aliasDesc = aliasSource ? readAttachString(aliasSource.DESC) : undefined;
|
|
1789
|
+
const aliasHtml = aliasSource ? readAttachString(aliasSource.HTML) : undefined;
|
|
1790
|
+
const aliasPreview = aliasSource ? readAttachString(aliasSource.PREVIEW) : undefined;
|
|
1791
|
+
const aliasWidth = aliasSource ? readAttachNumber(aliasSource.WIDTH) : undefined;
|
|
1792
|
+
const aliasHeight = aliasSource ? readAttachNumber(aliasSource.HEIGHT) : undefined;
|
|
1793
|
+
const aliasUserId = aliasSource ? readAttachNumber(aliasSource.USER_ID) : undefined;
|
|
1794
|
+
const aliasChatId = aliasSource ? readAttachNumber(aliasSource.CHAT_ID) : undefined;
|
|
1795
|
+
const aliasNetworkId = aliasSource ? readAttachString(aliasSource.NETWORK_ID) : undefined;
|
|
1796
|
+
return {
|
|
1797
|
+
LINK: link,
|
|
1798
|
+
...(aliasName ? { NAME: aliasName } : {}),
|
|
1799
|
+
...(aliasDesc ? { DESC: aliasDesc } : {}),
|
|
1800
|
+
...(aliasHtml ? { HTML: aliasHtml } : {}),
|
|
1801
|
+
...(aliasPreview ? { PREVIEW: aliasPreview } : {}),
|
|
1802
|
+
...(aliasWidth !== undefined ? { WIDTH: aliasWidth } : {}),
|
|
1803
|
+
...(aliasHeight !== undefined ? { HEIGHT: aliasHeight } : {}),
|
|
1804
|
+
...(aliasUserId !== undefined ? { USER_ID: aliasUserId } : {}),
|
|
1805
|
+
...(aliasChatId !== undefined ? { CHAT_ID: aliasChatId } : {}),
|
|
1806
|
+
...(aliasNetworkId ? { NETWORK_ID: aliasNetworkId } : {}),
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
if (!isPlainObject(rawValue)) {
|
|
1810
|
+
return undefined;
|
|
1811
|
+
}
|
|
1812
|
+
const link = readAttachString(rawValue.LINK);
|
|
1813
|
+
if (!link) {
|
|
1814
|
+
return undefined;
|
|
1815
|
+
}
|
|
1816
|
+
const name = readAttachString(rawValue.NAME) ?? readAttachString(rawValue.TITLE);
|
|
1817
|
+
const desc = readAttachString(rawValue.DESC);
|
|
1818
|
+
const html = readAttachString(rawValue.HTML);
|
|
1819
|
+
const preview = readAttachString(rawValue.PREVIEW);
|
|
1820
|
+
const width = readAttachNumber(rawValue.WIDTH);
|
|
1821
|
+
const height = readAttachNumber(rawValue.HEIGHT);
|
|
1822
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
1823
|
+
const chatId = readAttachNumber(rawValue.CHAT_ID);
|
|
1824
|
+
const networkId = readAttachString(rawValue.NETWORK_ID);
|
|
1825
|
+
return {
|
|
1826
|
+
LINK: link,
|
|
1827
|
+
...(name ? { NAME: name } : {}),
|
|
1828
|
+
...(desc ? { DESC: desc } : {}),
|
|
1829
|
+
...(html ? { HTML: html } : {}),
|
|
1830
|
+
...(preview ? { PREVIEW: preview } : {}),
|
|
1831
|
+
...(width !== undefined ? { WIDTH: width } : {}),
|
|
1832
|
+
...(height !== undefined ? { HEIGHT: height } : {}),
|
|
1833
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
1834
|
+
...(chatId !== undefined ? { CHAT_ID: chatId } : {}),
|
|
1835
|
+
...(networkId ? { NETWORK_ID: networkId } : {}),
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function normalizeAttachImageItem(rawValue) {
|
|
1839
|
+
const normalized = normalizeAttachLinkValue(rawValue);
|
|
1840
|
+
if (!normalized) {
|
|
1841
|
+
return undefined;
|
|
1842
|
+
}
|
|
1843
|
+
return {
|
|
1844
|
+
LINK: normalized.LINK,
|
|
1845
|
+
...(normalized.NAME ? { NAME: normalized.NAME } : {}),
|
|
1846
|
+
...(normalized.PREVIEW ? { PREVIEW: normalized.PREVIEW } : {}),
|
|
1847
|
+
...(normalized.WIDTH !== undefined ? { WIDTH: normalized.WIDTH } : {}),
|
|
1848
|
+
...(normalized.HEIGHT !== undefined ? { HEIGHT: normalized.HEIGHT } : {}),
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
function normalizeAttachFileItem(rawValue) {
|
|
1852
|
+
if (typeof rawValue === 'string') {
|
|
1853
|
+
const link = readAttachString(rawValue);
|
|
1854
|
+
return link ? { LINK: link } : undefined;
|
|
1855
|
+
}
|
|
1856
|
+
if (!isPlainObject(rawValue)) {
|
|
1857
|
+
return undefined;
|
|
1858
|
+
}
|
|
1859
|
+
const link = readAttachString(rawValue.LINK);
|
|
1860
|
+
if (!link) {
|
|
1861
|
+
return undefined;
|
|
1862
|
+
}
|
|
1863
|
+
const name = readAttachString(rawValue.NAME);
|
|
1864
|
+
const size = readAttachNumber(rawValue.SIZE);
|
|
1865
|
+
return {
|
|
1866
|
+
LINK: link,
|
|
1867
|
+
...(name ? { NAME: name } : {}),
|
|
1868
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function normalizeAttachUserValue(rawValue, aliasSource) {
|
|
1872
|
+
if (typeof rawValue === 'number' || typeof rawValue === 'string') {
|
|
1873
|
+
const userId = readAttachNumber(rawValue);
|
|
1874
|
+
if (userId === undefined) {
|
|
1875
|
+
return undefined;
|
|
1876
|
+
}
|
|
1877
|
+
const aliasName = aliasSource
|
|
1878
|
+
? readAttachString(aliasSource.NAME) ?? readAttachString(aliasSource.USER_NAME)
|
|
1879
|
+
: undefined;
|
|
1880
|
+
const aliasAvatar = aliasSource ? readAttachString(aliasSource.AVATAR) : undefined;
|
|
1881
|
+
const aliasLink = aliasSource ? readAttachString(aliasSource.LINK) : undefined;
|
|
1882
|
+
const aliasNetworkId = aliasSource ? readAttachString(aliasSource.NETWORK_ID) : undefined;
|
|
1883
|
+
return {
|
|
1884
|
+
USER_ID: userId,
|
|
1885
|
+
...(aliasName ? { NAME: aliasName } : {}),
|
|
1886
|
+
...(aliasAvatar ? { AVATAR: aliasAvatar } : {}),
|
|
1887
|
+
...(aliasLink ? { LINK: aliasLink } : {}),
|
|
1888
|
+
...(aliasNetworkId ? { NETWORK_ID: aliasNetworkId } : {}),
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
if (!isPlainObject(rawValue)) {
|
|
1892
|
+
return undefined;
|
|
1893
|
+
}
|
|
1894
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
1895
|
+
const name = readAttachString(rawValue.NAME) ?? readAttachString(rawValue.USER_NAME);
|
|
1896
|
+
const avatar = readAttachString(rawValue.AVATAR);
|
|
1897
|
+
const link = readAttachString(rawValue.LINK);
|
|
1898
|
+
const networkId = readAttachString(rawValue.NETWORK_ID);
|
|
1899
|
+
if (userId === undefined && !name && !avatar && !link && !networkId) {
|
|
1900
|
+
return undefined;
|
|
1901
|
+
}
|
|
1902
|
+
return {
|
|
1903
|
+
...(name ? { NAME: name } : {}),
|
|
1904
|
+
...(avatar ? { AVATAR: avatar } : {}),
|
|
1905
|
+
...(link ? { LINK: link } : {}),
|
|
1906
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
1907
|
+
...(networkId ? { NETWORK_ID: networkId } : {}),
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function normalizeAttachBlock(rawValue) {
|
|
1911
|
+
if (!isPlainObject(rawValue)) {
|
|
1912
|
+
return undefined;
|
|
1913
|
+
}
|
|
1914
|
+
const message = readAttachString(rawValue.MESSAGE);
|
|
1915
|
+
if (message && Object.keys(rawValue).every((key) => key === 'MESSAGE' || key === 'TYPE')) {
|
|
1916
|
+
return { MESSAGE: message };
|
|
1917
|
+
}
|
|
1918
|
+
if ('LINK' in rawValue && rawValue.LINK !== undefined) {
|
|
1919
|
+
const linkValue = normalizeAttachLinkValue(rawValue.LINK, rawValue);
|
|
1920
|
+
if (linkValue) {
|
|
1921
|
+
return { LINK: linkValue };
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
if ('IMAGE' in rawValue && rawValue.IMAGE !== undefined) {
|
|
1925
|
+
const rawImage = rawValue.IMAGE;
|
|
1926
|
+
const imageItems = Array.isArray(rawImage)
|
|
1927
|
+
? rawImage.map(normalizeAttachImageItem).filter(isPresent)
|
|
1928
|
+
: [normalizeAttachImageItem(rawImage)].filter(isPresent);
|
|
1929
|
+
if (imageItems.length > 0) {
|
|
1930
|
+
return { IMAGE: imageItems.length === 1 ? imageItems[0] : imageItems };
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if ('FILE' in rawValue && rawValue.FILE !== undefined) {
|
|
1934
|
+
const rawFile = rawValue.FILE;
|
|
1935
|
+
const fileItems = Array.isArray(rawFile)
|
|
1936
|
+
? rawFile.map(normalizeAttachFileItem).filter(isPresent)
|
|
1937
|
+
: [normalizeAttachFileItem(rawFile)].filter(isPresent);
|
|
1938
|
+
if (fileItems.length > 0) {
|
|
1939
|
+
return { FILE: fileItems.length === 1 ? fileItems[0] : fileItems };
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if ('DELIMITER' in rawValue && isPlainObject(rawValue.DELIMITER)) {
|
|
1943
|
+
const delimiter = rawValue.DELIMITER;
|
|
1944
|
+
const size = readAttachNumber(delimiter.SIZE);
|
|
1945
|
+
const color = readAttachString(delimiter.COLOR);
|
|
1946
|
+
if (size !== undefined || color) {
|
|
1947
|
+
return {
|
|
1948
|
+
DELIMITER: {
|
|
1949
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
1950
|
+
...(color ? { COLOR: color } : {}),
|
|
1951
|
+
},
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if ('GRID' in rawValue && Array.isArray(rawValue.GRID)) {
|
|
1956
|
+
const gridItems = rawValue.GRID.map(normalizeAttachGridItem).filter(isPresent);
|
|
1957
|
+
if (gridItems.length > 0) {
|
|
1958
|
+
return { GRID: gridItems };
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if ('USER' in rawValue && rawValue.USER !== undefined) {
|
|
1962
|
+
const userValue = normalizeAttachUserValue(rawValue.USER, rawValue);
|
|
1963
|
+
if (userValue) {
|
|
1964
|
+
return { USER: userValue };
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
const rawType = readAttachString(rawValue.TYPE)?.toUpperCase();
|
|
1968
|
+
if (!rawType) {
|
|
1969
|
+
return undefined;
|
|
1970
|
+
}
|
|
1971
|
+
if (rawType === 'MESSAGE') {
|
|
1972
|
+
const title = readAttachString(rawValue.TITLE);
|
|
1973
|
+
return message
|
|
1974
|
+
? { MESSAGE: message }
|
|
1975
|
+
: title
|
|
1976
|
+
? { MESSAGE: `[B]${title}[/B]` }
|
|
1977
|
+
: undefined;
|
|
1978
|
+
}
|
|
1979
|
+
if (rawType === 'TITLE') {
|
|
1980
|
+
const title = readAttachString(rawValue.TITLE) ?? message;
|
|
1981
|
+
return title ? { MESSAGE: `[B]${title}[/B]` } : undefined;
|
|
1982
|
+
}
|
|
1983
|
+
if (rawType === 'LINK') {
|
|
1984
|
+
const linkValue = normalizeAttachLinkValue(rawValue.LINK ?? rawValue, rawValue);
|
|
1985
|
+
return linkValue ? { LINK: linkValue } : undefined;
|
|
1986
|
+
}
|
|
1987
|
+
if (rawType === 'USER') {
|
|
1988
|
+
const userValue = normalizeAttachUserValue(rawValue.USER ?? rawValue.USER_ID ?? rawValue, rawValue);
|
|
1989
|
+
return userValue ? { USER: userValue } : undefined;
|
|
1990
|
+
}
|
|
1991
|
+
if (rawType === 'GRID') {
|
|
1992
|
+
const rawGridItems = Array.isArray(rawValue.GRID)
|
|
1993
|
+
? rawValue.GRID
|
|
1994
|
+
: Array.isArray(rawValue.ITEMS)
|
|
1995
|
+
? rawValue.ITEMS
|
|
1996
|
+
: Array.isArray(rawValue.ROWS)
|
|
1997
|
+
? rawValue.ROWS
|
|
1998
|
+
: [];
|
|
1999
|
+
const gridItems = rawGridItems.map(normalizeAttachGridItem).filter(isPresent);
|
|
2000
|
+
return gridItems.length > 0 ? { GRID: gridItems } : undefined;
|
|
2001
|
+
}
|
|
2002
|
+
if (rawType === 'DELIMITER') {
|
|
2003
|
+
const size = readAttachNumber(rawValue.SIZE);
|
|
2004
|
+
const color = readAttachString(rawValue.COLOR);
|
|
2005
|
+
return size !== undefined || color
|
|
2006
|
+
? {
|
|
2007
|
+
DELIMITER: {
|
|
2008
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
2009
|
+
...(color ? { COLOR: color } : {}),
|
|
2010
|
+
},
|
|
2011
|
+
}
|
|
2012
|
+
: undefined;
|
|
2013
|
+
}
|
|
2014
|
+
if (rawType === 'IMAGE') {
|
|
2015
|
+
const rawImage = rawValue.IMAGE ?? rawValue.LINK ?? rawValue;
|
|
2016
|
+
const imageItems = Array.isArray(rawImage)
|
|
2017
|
+
? rawImage.map(normalizeAttachImageItem).filter(isPresent)
|
|
2018
|
+
: [normalizeAttachImageItem(rawImage)].filter(isPresent);
|
|
2019
|
+
return imageItems.length > 0
|
|
2020
|
+
? { IMAGE: imageItems.length === 1 ? imageItems[0] : imageItems }
|
|
2021
|
+
: undefined;
|
|
2022
|
+
}
|
|
2023
|
+
if (rawType === 'FILE') {
|
|
2024
|
+
const rawFile = rawValue.FILE ?? rawValue.LINK ?? rawValue;
|
|
2025
|
+
const fileItems = Array.isArray(rawFile)
|
|
2026
|
+
? rawFile.map(normalizeAttachFileItem).filter(isPresent)
|
|
2027
|
+
: [normalizeAttachFileItem(rawFile)].filter(isPresent);
|
|
2028
|
+
return fileItems.length > 0
|
|
2029
|
+
? { FILE: fileItems.length === 1 ? fileItems[0] : fileItems }
|
|
2030
|
+
: undefined;
|
|
2031
|
+
}
|
|
2032
|
+
return undefined;
|
|
2033
|
+
}
|
|
2034
|
+
function normalizeBitrix24Attach(rawValue) {
|
|
2035
|
+
if (Array.isArray(rawValue)) {
|
|
2036
|
+
const blocks = rawValue.map(normalizeAttachBlock).filter(isPresent);
|
|
2037
|
+
return blocks.length > 0 ? blocks : undefined;
|
|
2038
|
+
}
|
|
2039
|
+
if (!isPlainObject(rawValue)) {
|
|
2040
|
+
return undefined;
|
|
2041
|
+
}
|
|
2042
|
+
if (Array.isArray(rawValue.BLOCKS)) {
|
|
2043
|
+
const blocks = rawValue.BLOCKS.map(normalizeAttachBlock).filter(isPresent);
|
|
2044
|
+
if (blocks.length === 0) {
|
|
2045
|
+
return undefined;
|
|
2046
|
+
}
|
|
2047
|
+
const id = readAttachNumber(rawValue.ID);
|
|
2048
|
+
const colorToken = normalizeAttachColorTokenValue(rawValue.COLOR_TOKEN);
|
|
2049
|
+
const color = readAttachString(rawValue.COLOR);
|
|
2050
|
+
return {
|
|
2051
|
+
...(id !== undefined ? { ID: id } : {}),
|
|
2052
|
+
...(colorToken ? { COLOR_TOKEN: colorToken } : {}),
|
|
2053
|
+
...(color ? { COLOR: color } : {}),
|
|
2054
|
+
BLOCKS: blocks,
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
const block = normalizeAttachBlock(rawValue);
|
|
2058
|
+
return block ? [block] : undefined;
|
|
2059
|
+
}
|
|
1381
2060
|
function parseActionAttach(rawValue) {
|
|
1382
2061
|
if (rawValue == null) {
|
|
1383
2062
|
return undefined;
|
|
@@ -1395,7 +2074,7 @@ function parseActionAttach(rawValue) {
|
|
|
1395
2074
|
return undefined;
|
|
1396
2075
|
}
|
|
1397
2076
|
}
|
|
1398
|
-
return
|
|
2077
|
+
return normalizeBitrix24Attach(parsed);
|
|
1399
2078
|
}
|
|
1400
2079
|
function hasMeaningfulAttachInput(rawValue) {
|
|
1401
2080
|
if (rawValue == null) {
|
|
@@ -1523,8 +2202,11 @@ function collectActionMediaUrls(params) {
|
|
|
1523
2202
|
};
|
|
1524
2203
|
append(params.mediaUrl);
|
|
1525
2204
|
append(params.mediaUrls);
|
|
2205
|
+
append(params.media);
|
|
1526
2206
|
append(params.filePath);
|
|
1527
2207
|
append(params.filePaths);
|
|
2208
|
+
append(params.path);
|
|
2209
|
+
append(params.paths);
|
|
1528
2210
|
return mediaUrls;
|
|
1529
2211
|
}
|
|
1530
2212
|
function buildActionMessageText(params) {
|
|
@@ -1941,16 +2623,22 @@ async function uploadOutboundMedia(params) {
|
|
|
1941
2623
|
let lastMessageId = '';
|
|
1942
2624
|
let message = params.initialMessage;
|
|
1943
2625
|
for (const mediaUrl of params.mediaUrls) {
|
|
2626
|
+
const outboundFileName = resolveSemanticOutboundFileName({
|
|
2627
|
+
requestedFileName: basename(mediaUrl),
|
|
2628
|
+
dialogId: params.sendCtx.dialogId,
|
|
2629
|
+
currentMessageId: params.currentMessageId,
|
|
2630
|
+
accountId: params.accountId,
|
|
2631
|
+
});
|
|
1944
2632
|
const result = await params.mediaService.uploadMediaToChat({
|
|
1945
2633
|
localPath: mediaUrl,
|
|
1946
|
-
fileName:
|
|
2634
|
+
fileName: outboundFileName,
|
|
1947
2635
|
webhookUrl: params.sendCtx.webhookUrl,
|
|
1948
2636
|
bot: params.sendCtx.bot,
|
|
1949
2637
|
dialogId: params.sendCtx.dialogId,
|
|
1950
2638
|
message: message || undefined,
|
|
1951
2639
|
});
|
|
1952
2640
|
if (!result.ok) {
|
|
1953
|
-
throw new Error(`Failed to upload media: ${
|
|
2641
|
+
throw new Error(`Failed to upload media: ${outboundFileName}`);
|
|
1954
2642
|
}
|
|
1955
2643
|
if (result.messageId) {
|
|
1956
2644
|
lastMessageId = String(result.messageId);
|
|
@@ -2055,6 +2743,8 @@ export const bitrix24Plugin = {
|
|
|
2055
2743
|
mediaService: gatewayState.mediaService,
|
|
2056
2744
|
sendCtx,
|
|
2057
2745
|
mediaUrls,
|
|
2746
|
+
currentMessageId: ctx.currentMessageId,
|
|
2747
|
+
accountId: ctx.accountId,
|
|
2058
2748
|
initialMessage: ctx.text,
|
|
2059
2749
|
});
|
|
2060
2750
|
return { messageId };
|
|
@@ -2086,6 +2776,8 @@ export const bitrix24Plugin = {
|
|
|
2086
2776
|
mediaService: gatewayState.mediaService,
|
|
2087
2777
|
sendCtx,
|
|
2088
2778
|
mediaUrls,
|
|
2779
|
+
currentMessageId: ctx.currentMessageId,
|
|
2780
|
+
accountId: ctx.accountId,
|
|
2089
2781
|
initialMessage,
|
|
2090
2782
|
});
|
|
2091
2783
|
if ((text && !initialMessage) || keyboard || attach) {
|
|
@@ -2131,6 +2823,9 @@ export const bitrix24Plugin = {
|
|
|
2131
2823
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
2132
2824
|
details: payload,
|
|
2133
2825
|
});
|
|
2826
|
+
const markSuccessfulAction = (currentMessageId) => {
|
|
2827
|
+
markRecentSuccessfulAction(currentMessageId, ctx.accountId);
|
|
2828
|
+
};
|
|
2134
2829
|
// ─── Send with buttons ──────────────────────────────────────────────
|
|
2135
2830
|
if (ctx.action === 'send') {
|
|
2136
2831
|
const rawButtons = ctx.params.buttons;
|
|
@@ -2145,6 +2840,7 @@ export const bitrix24Plugin = {
|
|
|
2145
2840
|
return null;
|
|
2146
2841
|
}
|
|
2147
2842
|
const sendCtx = { webhookUrl: config.webhookUrl, bot, dialogId: to };
|
|
2843
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ dialogId: to });
|
|
2148
2844
|
const messageText = readActionTextParam(ctx.params);
|
|
2149
2845
|
const message = typeof messageText === 'string' ? messageText.trim() : '';
|
|
2150
2846
|
const keyboard = parseActionKeyboard(rawButtons);
|
|
@@ -2176,6 +2872,25 @@ export const bitrix24Plugin = {
|
|
|
2176
2872
|
hint: 'Provide message text, buttons, rich attach blocks or mediaUrl/mediaUrls for Bitrix24 send.',
|
|
2177
2873
|
});
|
|
2178
2874
|
}
|
|
2875
|
+
const duplicateMediaDelivery = mediaUrls.length > 0
|
|
2876
|
+
? findRecentMediaDelivery({
|
|
2877
|
+
dialogId: to,
|
|
2878
|
+
currentMessageId: currentToolMessageId,
|
|
2879
|
+
mediaUrls,
|
|
2880
|
+
accountId: ctx.accountId,
|
|
2881
|
+
})
|
|
2882
|
+
: null;
|
|
2883
|
+
if (duplicateMediaDelivery) {
|
|
2884
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2885
|
+
return toolResult({
|
|
2886
|
+
channel: 'bitrix24',
|
|
2887
|
+
to,
|
|
2888
|
+
via: 'direct',
|
|
2889
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
2890
|
+
duplicateSuppressed: true,
|
|
2891
|
+
result: { messageId: duplicateMediaDelivery.messageId ?? '' },
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2179
2894
|
try {
|
|
2180
2895
|
if (explicitForwardMessages.length > 0) {
|
|
2181
2896
|
const normalizedForwardText = normalizeForwardActionText(messageText);
|
|
@@ -2185,7 +2900,16 @@ export const bitrix24Plugin = {
|
|
|
2185
2900
|
attach,
|
|
2186
2901
|
forwardMessages: explicitForwardMessages,
|
|
2187
2902
|
});
|
|
2903
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
2904
|
+
api: gatewayState.api,
|
|
2905
|
+
sendService: gatewayState.sendService,
|
|
2906
|
+
webhookUrl: config.webhookUrl,
|
|
2907
|
+
bot,
|
|
2908
|
+
dialogId: forwardAckDialogId,
|
|
2909
|
+
currentMessageId: currentToolMessageId,
|
|
2910
|
+
});
|
|
2188
2911
|
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2912
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2189
2913
|
return toolResult({
|
|
2190
2914
|
channel: 'bitrix24',
|
|
2191
2915
|
to,
|
|
@@ -2200,7 +2924,16 @@ export const bitrix24Plugin = {
|
|
|
2200
2924
|
...(keyboard ? { keyboard } : {}),
|
|
2201
2925
|
forwardMessages: explicitForwardMessages,
|
|
2202
2926
|
});
|
|
2927
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
2928
|
+
api: gatewayState.api,
|
|
2929
|
+
sendService: gatewayState.sendService,
|
|
2930
|
+
webhookUrl: config.webhookUrl,
|
|
2931
|
+
bot,
|
|
2932
|
+
dialogId: forwardAckDialogId,
|
|
2933
|
+
currentMessageId: currentToolMessageId,
|
|
2934
|
+
});
|
|
2203
2935
|
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2936
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2204
2937
|
return toolResult({
|
|
2205
2938
|
channel: 'bitrix24',
|
|
2206
2939
|
to,
|
|
@@ -2211,7 +2944,7 @@ export const bitrix24Plugin = {
|
|
|
2211
2944
|
result: { messageId: String(result.messageId ?? '') },
|
|
2212
2945
|
});
|
|
2213
2946
|
}
|
|
2214
|
-
if (referencedReplyMessageId && !attach && mediaUrls.length === 0) {
|
|
2947
|
+
if (canUseNativeReply && referencedReplyMessageId && !attach && mediaUrls.length === 0) {
|
|
2215
2948
|
let referencedMessageText = '';
|
|
2216
2949
|
try {
|
|
2217
2950
|
const referencedMessage = await gatewayState.api.getMessage(config.webhookUrl, bot, referencedReplyMessageId);
|
|
@@ -2224,7 +2957,16 @@ export const bitrix24Plugin = {
|
|
|
2224
2957
|
const normalizedMessage = normalizeComparableMessageText(message);
|
|
2225
2958
|
if (normalizedReferencedMessage && normalizedReferencedMessage === normalizedMessage) {
|
|
2226
2959
|
const result = await gatewayState.sendService.sendText(sendCtx, '', { forwardMessages: [referencedReplyMessageId] });
|
|
2960
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
2961
|
+
api: gatewayState.api,
|
|
2962
|
+
sendService: gatewayState.sendService,
|
|
2963
|
+
webhookUrl: config.webhookUrl,
|
|
2964
|
+
bot,
|
|
2965
|
+
dialogId: forwardAckDialogId,
|
|
2966
|
+
currentMessageId: currentToolMessageId,
|
|
2967
|
+
});
|
|
2227
2968
|
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2969
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2228
2970
|
return toolResult({
|
|
2229
2971
|
channel: 'bitrix24',
|
|
2230
2972
|
to,
|
|
@@ -2235,17 +2977,21 @@ export const bitrix24Plugin = {
|
|
|
2235
2977
|
result: { messageId: String(result.messageId ?? '') },
|
|
2236
2978
|
});
|
|
2237
2979
|
}
|
|
2238
|
-
const
|
|
2980
|
+
const sendOptions = {
|
|
2239
2981
|
...(keyboard ? { keyboard } : {}),
|
|
2240
|
-
replyToMessageId: referencedReplyMessageId,
|
|
2241
|
-
}
|
|
2982
|
+
...(canUseNativeReply ? { replyToMessageId: referencedReplyMessageId } : {}),
|
|
2983
|
+
};
|
|
2984
|
+
const result = await gatewayState.sendService.sendText(sendCtx, message || ' ', Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
2985
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2242
2986
|
return toolResult({
|
|
2243
2987
|
channel: 'bitrix24',
|
|
2244
2988
|
to,
|
|
2245
2989
|
via: 'direct',
|
|
2246
2990
|
mediaUrl: null,
|
|
2247
|
-
|
|
2248
|
-
|
|
2991
|
+
...(canUseNativeReply ? {
|
|
2992
|
+
replied: true,
|
|
2993
|
+
replyToMessageId: referencedReplyMessageId,
|
|
2994
|
+
} : {}),
|
|
2249
2995
|
result: { messageId: String(result.messageId ?? '') },
|
|
2250
2996
|
});
|
|
2251
2997
|
}
|
|
@@ -2260,7 +3006,16 @@ export const bitrix24Plugin = {
|
|
|
2260
3006
|
&& normalizeComparableMessageText(message) === normalizeComparableMessageText(previousInboundMessage.body)
|
|
2261
3007
|
&& normalizeComparableMessageText(currentInboundContext.body) !== normalizeComparableMessageText(message)) {
|
|
2262
3008
|
const result = await gatewayState.sendService.sendText(sendCtx, '', { forwardMessages: [Number(previousInboundMessage.messageId)] });
|
|
3009
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
3010
|
+
api: gatewayState.api,
|
|
3011
|
+
sendService: gatewayState.sendService,
|
|
3012
|
+
webhookUrl: config.webhookUrl,
|
|
3013
|
+
bot,
|
|
3014
|
+
dialogId: forwardAckDialogId,
|
|
3015
|
+
currentMessageId: currentToolMessageId,
|
|
3016
|
+
});
|
|
2263
3017
|
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
3018
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2264
3019
|
return toolResult({
|
|
2265
3020
|
channel: 'bitrix24',
|
|
2266
3021
|
to,
|
|
@@ -2277,14 +3032,24 @@ export const bitrix24Plugin = {
|
|
|
2277
3032
|
mediaService: gatewayState.mediaService,
|
|
2278
3033
|
sendCtx,
|
|
2279
3034
|
mediaUrls,
|
|
3035
|
+
currentMessageId: currentToolMessageId,
|
|
3036
|
+
accountId: ctx.accountId,
|
|
2280
3037
|
initialMessage,
|
|
2281
3038
|
});
|
|
3039
|
+
markRecentMediaDelivery({
|
|
3040
|
+
dialogId: to,
|
|
3041
|
+
currentMessageId: currentToolMessageId,
|
|
3042
|
+
mediaUrls,
|
|
3043
|
+
accountId: ctx.accountId,
|
|
3044
|
+
messageId: uploadedMessageId,
|
|
3045
|
+
});
|
|
2282
3046
|
if ((message && !initialMessage) || keyboard || attach) {
|
|
2283
3047
|
if (attach) {
|
|
2284
3048
|
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: message, keyboard, attach }), {
|
|
2285
3049
|
...(keyboard ? { keyboard } : {}),
|
|
2286
3050
|
attach,
|
|
2287
3051
|
});
|
|
3052
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2288
3053
|
return toolResult({
|
|
2289
3054
|
channel: 'bitrix24',
|
|
2290
3055
|
to,
|
|
@@ -2294,6 +3059,7 @@ export const bitrix24Plugin = {
|
|
|
2294
3059
|
});
|
|
2295
3060
|
}
|
|
2296
3061
|
const result = await gatewayState.sendService.sendText(sendCtx, message || '', keyboard ? { keyboard } : undefined);
|
|
3062
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2297
3063
|
return toolResult({
|
|
2298
3064
|
channel: 'bitrix24',
|
|
2299
3065
|
to,
|
|
@@ -2302,6 +3068,7 @@ export const bitrix24Plugin = {
|
|
|
2302
3068
|
result: { messageId: String(result.messageId ?? uploadedMessageId) },
|
|
2303
3069
|
});
|
|
2304
3070
|
}
|
|
3071
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2305
3072
|
return toolResult({
|
|
2306
3073
|
channel: 'bitrix24',
|
|
2307
3074
|
to,
|
|
@@ -2315,6 +3082,7 @@ export const bitrix24Plugin = {
|
|
|
2315
3082
|
...(keyboard ? { keyboard } : {}),
|
|
2316
3083
|
attach,
|
|
2317
3084
|
});
|
|
3085
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2318
3086
|
return toolResult({
|
|
2319
3087
|
channel: 'bitrix24',
|
|
2320
3088
|
to,
|
|
@@ -2324,6 +3092,7 @@ export const bitrix24Plugin = {
|
|
|
2324
3092
|
});
|
|
2325
3093
|
}
|
|
2326
3094
|
const result = await gatewayState.sendService.sendText(sendCtx, message || ' ', keyboard ? { keyboard } : undefined);
|
|
3095
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2327
3096
|
return toolResult({
|
|
2328
3097
|
channel: 'bitrix24',
|
|
2329
3098
|
to,
|
|
@@ -2334,6 +3103,14 @@ export const bitrix24Plugin = {
|
|
|
2334
3103
|
}
|
|
2335
3104
|
catch (err) {
|
|
2336
3105
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3106
|
+
if (errMsg.startsWith('Failed to upload media: ')) {
|
|
3107
|
+
return toolResult({
|
|
3108
|
+
ok: false,
|
|
3109
|
+
reason: 'media_upload_failed',
|
|
3110
|
+
error: errMsg,
|
|
3111
|
+
hint: 'The local file could not be uploaded. First create/write the file in the OpenClaw workspace or managed media directory, then send it once with media/filePath/path. Do not retry the same send blindly, and do not send an extra confirmation after a successful file upload.',
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
2337
3114
|
return toolResult({ ok: false, error: errMsg });
|
|
2338
3115
|
}
|
|
2339
3116
|
}
|
|
@@ -2349,6 +3126,7 @@ export const bitrix24Plugin = {
|
|
|
2349
3126
|
return toolResult({ ok: false, reason: 'missing_target', hint: 'Bitrix24 reply requires a target dialog id in "to" or "target". Do not retry.' });
|
|
2350
3127
|
}
|
|
2351
3128
|
const toolContext = ctx.toolContext;
|
|
3129
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ dialogId: to });
|
|
2352
3130
|
const replyToMessageId = parseActionMessageIds(params.replyToMessageId
|
|
2353
3131
|
?? params.replyToId
|
|
2354
3132
|
?? params.messageId
|
|
@@ -2373,25 +3151,32 @@ export const bitrix24Plugin = {
|
|
|
2373
3151
|
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: normalizedText, keyboard, attach }), {
|
|
2374
3152
|
...(keyboard ? { keyboard } : {}),
|
|
2375
3153
|
attach,
|
|
2376
|
-
replyToMessageId,
|
|
3154
|
+
...(canUseNativeReply ? { replyToMessageId } : {}),
|
|
2377
3155
|
});
|
|
3156
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
2378
3157
|
return toolResult({
|
|
2379
3158
|
ok: true,
|
|
2380
|
-
replied: true,
|
|
2381
3159
|
to,
|
|
2382
|
-
|
|
3160
|
+
...(canUseNativeReply ? {
|
|
3161
|
+
replied: true,
|
|
3162
|
+
replyToMessageId,
|
|
3163
|
+
} : {}),
|
|
2383
3164
|
messageId: String(messageId ?? ''),
|
|
2384
3165
|
});
|
|
2385
3166
|
}
|
|
2386
|
-
const
|
|
3167
|
+
const sendOptions = {
|
|
2387
3168
|
...(keyboard ? { keyboard } : {}),
|
|
2388
|
-
replyToMessageId,
|
|
2389
|
-
}
|
|
3169
|
+
...(canUseNativeReply ? { replyToMessageId } : {}),
|
|
3170
|
+
};
|
|
3171
|
+
const result = await gatewayState.sendService.sendText({ webhookUrl: config.webhookUrl, bot, dialogId: to }, normalizedText || ' ', Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
3172
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
2390
3173
|
return toolResult({
|
|
2391
3174
|
ok: true,
|
|
2392
|
-
replied: true,
|
|
2393
3175
|
to,
|
|
2394
|
-
|
|
3176
|
+
...(canUseNativeReply ? {
|
|
3177
|
+
replied: true,
|
|
3178
|
+
replyToMessageId,
|
|
3179
|
+
} : {}),
|
|
2395
3180
|
messageId: String(result.messageId ?? ''),
|
|
2396
3181
|
});
|
|
2397
3182
|
}
|
|
@@ -2449,6 +3234,7 @@ export const bitrix24Plugin = {
|
|
|
2449
3234
|
...(keyboard ? { keyboard } : {}),
|
|
2450
3235
|
...(attach ? { attach } : {}),
|
|
2451
3236
|
});
|
|
3237
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
2452
3238
|
return toolResult({ ok: true, edited: true, messageId });
|
|
2453
3239
|
}
|
|
2454
3240
|
catch (err) {
|
|
@@ -2477,6 +3263,7 @@ export const bitrix24Plugin = {
|
|
|
2477
3263
|
|| params.complete === 'Y');
|
|
2478
3264
|
try {
|
|
2479
3265
|
await api.deleteMessage(config.webhookUrl, bot, messageId, complete);
|
|
3266
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
2480
3267
|
return toolResult({ ok: true, deleted: true, messageId });
|
|
2481
3268
|
}
|
|
2482
3269
|
catch (err) {
|
|
@@ -2498,6 +3285,10 @@ export const bitrix24Plugin = {
|
|
|
2498
3285
|
const toolContext = ctx.toolContext;
|
|
2499
3286
|
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
2500
3287
|
const messageId = Number(rawMessageId);
|
|
3288
|
+
const reactionAckSourceMessageId = typeof rawMessageId === 'string' || typeof rawMessageId === 'number'
|
|
3289
|
+
? rawMessageId
|
|
3290
|
+
: toolContext?.currentMessageId;
|
|
3291
|
+
const reactionAckDialogId = resolveRecentInboundDialogId(reactionAckSourceMessageId, ctx.accountId);
|
|
2501
3292
|
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
2502
3293
|
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 reactions. Do not retry.' });
|
|
2503
3294
|
}
|
|
@@ -2511,6 +3302,7 @@ export const bitrix24Plugin = {
|
|
|
2511
3302
|
}
|
|
2512
3303
|
try {
|
|
2513
3304
|
await api.deleteReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
3305
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
2514
3306
|
return toolResult({ ok: true, removed: true });
|
|
2515
3307
|
}
|
|
2516
3308
|
catch (err) {
|
|
@@ -2533,14 +3325,28 @@ export const bitrix24Plugin = {
|
|
|
2533
3325
|
}
|
|
2534
3326
|
try {
|
|
2535
3327
|
await api.addReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
3328
|
+
await maybeSendReactionTypingReset({
|
|
3329
|
+
sendService: gatewayState.sendService,
|
|
3330
|
+
webhookUrl: config.webhookUrl,
|
|
3331
|
+
bot,
|
|
3332
|
+
dialogId: reactionAckDialogId,
|
|
3333
|
+
});
|
|
2536
3334
|
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
3335
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
2537
3336
|
return toolResult({ ok: true, added: emoji });
|
|
2538
3337
|
}
|
|
2539
3338
|
catch (err) {
|
|
2540
3339
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2541
3340
|
const isAlreadySet = errMsg.includes('REACTION_ALREADY_SET');
|
|
2542
3341
|
if (isAlreadySet) {
|
|
3342
|
+
await maybeSendReactionTypingReset({
|
|
3343
|
+
sendService: gatewayState.sendService,
|
|
3344
|
+
webhookUrl: config.webhookUrl,
|
|
3345
|
+
bot,
|
|
3346
|
+
dialogId: reactionAckDialogId,
|
|
3347
|
+
});
|
|
2543
3348
|
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
3349
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
2544
3350
|
return toolResult({ ok: true, added: emoji, warning: 'Reaction already set.' });
|
|
2545
3351
|
}
|
|
2546
3352
|
return toolResult({ ok: false, reason: 'error', emoji, hint: `Reaction failed: ${errMsg}. Do not retry.` });
|
|
@@ -2678,6 +3484,30 @@ export const bitrix24Plugin = {
|
|
|
2678
3484
|
}
|
|
2679
3485
|
const sendService = new SendService(api, logger);
|
|
2680
3486
|
const mediaService = new MediaService(api, logger);
|
|
3487
|
+
const originalSendText = sendService.sendText.bind(sendService);
|
|
3488
|
+
sendService.sendText = async (...args) => {
|
|
3489
|
+
const result = await originalSendText(...args);
|
|
3490
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
3491
|
+
return result;
|
|
3492
|
+
};
|
|
3493
|
+
const originalAnswerCommandText = sendService.answerCommandText.bind(sendService);
|
|
3494
|
+
sendService.answerCommandText = async (...args) => {
|
|
3495
|
+
const result = await originalAnswerCommandText(...args);
|
|
3496
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
3497
|
+
return result;
|
|
3498
|
+
};
|
|
3499
|
+
const originalApiSendMessage = api.sendMessage.bind(api);
|
|
3500
|
+
api.sendMessage = async (...args) => {
|
|
3501
|
+
const messageId = await originalApiSendMessage(...args);
|
|
3502
|
+
markRecentOutboundMessage(messageId, ctx.accountId);
|
|
3503
|
+
return messageId;
|
|
3504
|
+
};
|
|
3505
|
+
const originalUploadMediaToChat = mediaService.uploadMediaToChat.bind(mediaService);
|
|
3506
|
+
mediaService.uploadMediaToChat = async (...args) => {
|
|
3507
|
+
const result = await originalUploadMediaToChat(...args);
|
|
3508
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
3509
|
+
return result;
|
|
3510
|
+
};
|
|
2681
3511
|
const hydrateReplyEntry = async (replyToMessageId) => {
|
|
2682
3512
|
const bitrixMessageId = toMessageId(replyToMessageId);
|
|
2683
3513
|
if (!bitrixMessageId) {
|
|
@@ -2840,8 +3670,12 @@ export const bitrix24Plugin = {
|
|
|
2840
3670
|
query: bodyWithReply,
|
|
2841
3671
|
historyCache,
|
|
2842
3672
|
});
|
|
3673
|
+
const inlineButtonsAgentHint = buildBitrix24InlineButtonsAgentHint();
|
|
2843
3674
|
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
2844
|
-
const
|
|
3675
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ isDm: msgCtx.isDm });
|
|
3676
|
+
const nativeReplyAgentHint = canUseNativeReply
|
|
3677
|
+
? buildBitrix24NativeReplyAgentHint(msgCtx.messageId)
|
|
3678
|
+
: undefined;
|
|
2845
3679
|
const nativeForwardAgentHint = buildBitrix24NativeForwardAgentHint({
|
|
2846
3680
|
accountId: ctx.accountId,
|
|
2847
3681
|
currentBody: body,
|
|
@@ -2850,6 +3684,7 @@ export const bitrix24Plugin = {
|
|
|
2850
3684
|
historyEntries: previousEntries,
|
|
2851
3685
|
});
|
|
2852
3686
|
const bodyForAgent = [
|
|
3687
|
+
inlineButtonsAgentHint,
|
|
2853
3688
|
fileDeliveryAgentHint,
|
|
2854
3689
|
nativeReplyAgentHint,
|
|
2855
3690
|
nativeForwardAgentHint,
|
|
@@ -2863,7 +3698,7 @@ export const bitrix24Plugin = {
|
|
|
2863
3698
|
})
|
|
2864
3699
|
: bodyForAgent;
|
|
2865
3700
|
recordHistory(body);
|
|
2866
|
-
rememberRecentInboundMessage(conversation.dialogId, msgCtx.messageId, body, msgCtx.timestamp ?? Date.now(), ctx.accountId);
|
|
3701
|
+
rememberRecentInboundMessage(conversation.dialogId, msgCtx.messageId, body, msgCtx.media.map((mediaItem) => mediaItem.name), msgCtx.timestamp ?? Date.now(), ctx.accountId);
|
|
2867
3702
|
// Resolve which agent handles this conversation
|
|
2868
3703
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
2869
3704
|
cfg,
|
|
@@ -2916,14 +3751,32 @@ export const bitrix24Plugin = {
|
|
|
2916
3751
|
deliver: async (payload) => {
|
|
2917
3752
|
await replyStatusHeartbeat.stopAndWait();
|
|
2918
3753
|
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
3754
|
+
const hadRecentMediaDelivery = mediaUrls.length === 0
|
|
3755
|
+
&& hasRecentMediaDelivery(sendCtx.dialogId, msgCtx.messageId, ctx.accountId);
|
|
2919
3756
|
if (mediaUrls.length > 0) {
|
|
2920
|
-
await uploadOutboundMedia({
|
|
3757
|
+
const uploadedMessageId = await uploadOutboundMedia({
|
|
2921
3758
|
mediaService,
|
|
2922
3759
|
sendCtx,
|
|
2923
3760
|
mediaUrls,
|
|
2924
3761
|
});
|
|
3762
|
+
markRecentMediaDelivery({
|
|
3763
|
+
dialogId: sendCtx.dialogId,
|
|
3764
|
+
currentMessageId: msgCtx.messageId,
|
|
3765
|
+
mediaUrls,
|
|
3766
|
+
accountId: ctx.accountId,
|
|
3767
|
+
messageId: uploadedMessageId,
|
|
3768
|
+
});
|
|
2925
3769
|
}
|
|
2926
3770
|
if (payload.text) {
|
|
3771
|
+
if (hadRecentMediaDelivery) {
|
|
3772
|
+
replyDelivered = true;
|
|
3773
|
+
logger.debug('Suppressing trailing text after successful Bitrix24 file upload', {
|
|
3774
|
+
senderId: msgCtx.senderId,
|
|
3775
|
+
chatId: msgCtx.chatId,
|
|
3776
|
+
messageId: msgCtx.messageId,
|
|
3777
|
+
});
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
2927
3780
|
if (consumePendingNativeForwardAck(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
2928
3781
|
replyDelivered = true;
|
|
2929
3782
|
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 forward', {
|
|
@@ -2975,7 +3828,7 @@ export const bitrix24Plugin = {
|
|
|
2975
3828
|
: [];
|
|
2976
3829
|
const nativeReplyToMessageId = nativeForwardMessageId
|
|
2977
3830
|
? undefined
|
|
2978
|
-
: replyDirective.replyToMessageId;
|
|
3831
|
+
: (canUseNativeReply ? replyDirective.replyToMessageId : undefined);
|
|
2979
3832
|
const sendOptions = {
|
|
2980
3833
|
...(keyboard ? { keyboard } : {}),
|
|
2981
3834
|
...(nativeReplyToMessageId ? { replyToMessageId: nativeReplyToMessageId } : {}),
|
|
@@ -3013,18 +3866,55 @@ export const bitrix24Plugin = {
|
|
|
3013
3866
|
},
|
|
3014
3867
|
},
|
|
3015
3868
|
});
|
|
3869
|
+
if (!replyDelivered && hasRecentMediaDelivery(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
3870
|
+
replyDelivered = true;
|
|
3871
|
+
logger.debug('Suppressing fallback after successful Bitrix24 file upload in the same turn', {
|
|
3872
|
+
senderId: msgCtx.senderId,
|
|
3873
|
+
chatId: msgCtx.chatId,
|
|
3874
|
+
messageId: msgCtx.messageId,
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
if (!replyDelivered && hasRecentSuccessfulAction(msgCtx.messageId, ctx.accountId)) {
|
|
3878
|
+
replyDelivered = true;
|
|
3879
|
+
logger.debug('Suppressing fallback after successful Bitrix24 action in the same turn', {
|
|
3880
|
+
senderId: msgCtx.senderId,
|
|
3881
|
+
chatId: msgCtx.chatId,
|
|
3882
|
+
messageId: msgCtx.messageId,
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
const shouldWaitForEmptyReplyFallback = !replyDelivered
|
|
3886
|
+
&& isEmptyDispatchResult(dispatchResult);
|
|
3016
3887
|
if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3017
3888
|
logger.debug('Reply completed without queued final block, waiting before fallback', {
|
|
3018
3889
|
senderId: msgCtx.senderId,
|
|
3019
3890
|
chatId: msgCtx.chatId,
|
|
3020
3891
|
counts: dispatchResult.counts,
|
|
3892
|
+
fallbackDelayMs: shouldWaitForEmptyReplyFallback
|
|
3893
|
+
? EMPTY_REPLY_FALLBACK_GRACE_MS
|
|
3894
|
+
: REPLY_FALLBACK_GRACE_MS,
|
|
3021
3895
|
});
|
|
3022
|
-
|
|
3896
|
+
if (shouldWaitForEmptyReplyFallback) {
|
|
3897
|
+
await waitEmptyReplyFallbackGraceWindow();
|
|
3898
|
+
}
|
|
3899
|
+
else {
|
|
3900
|
+
await waitReplyFallbackGraceWindow();
|
|
3901
|
+
}
|
|
3023
3902
|
}
|
|
3024
|
-
if (!replyDelivered && dispatchResult
|
|
3903
|
+
if (!replyDelivered && isEmptyDispatchResult(dispatchResult)) {
|
|
3904
|
+
replyDelivered = true;
|
|
3905
|
+
logger.warn('Reply completed without any user-visible payload or tool activity; sending fallback notice', {
|
|
3906
|
+
senderId: msgCtx.senderId,
|
|
3907
|
+
chatId: msgCtx.chatId,
|
|
3908
|
+
messageId: msgCtx.messageId,
|
|
3909
|
+
counts: dispatchResult.counts,
|
|
3910
|
+
});
|
|
3911
|
+
await sendService.sendText(sendCtx, emptyReplyFallback(msgCtx.language));
|
|
3912
|
+
}
|
|
3913
|
+
else if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3025
3914
|
logger.warn('Reply completed without a final user-visible message; fallback notice suppressed', {
|
|
3026
3915
|
senderId: msgCtx.senderId,
|
|
3027
3916
|
chatId: msgCtx.chatId,
|
|
3917
|
+
messageId: msgCtx.messageId,
|
|
3028
3918
|
counts: dispatchResult.counts,
|
|
3029
3919
|
});
|
|
3030
3920
|
}
|
|
@@ -3032,6 +3922,7 @@ export const bitrix24Plugin = {
|
|
|
3032
3922
|
logger.debug('Late reply arrived during fallback grace window, skipping fallback', {
|
|
3033
3923
|
senderId: msgCtx.senderId,
|
|
3034
3924
|
chatId: msgCtx.chatId,
|
|
3925
|
+
messageId: msgCtx.messageId,
|
|
3035
3926
|
});
|
|
3036
3927
|
}
|
|
3037
3928
|
}
|
|
@@ -3050,7 +3941,7 @@ export const bitrix24Plugin = {
|
|
|
3050
3941
|
}
|
|
3051
3942
|
}
|
|
3052
3943
|
finally {
|
|
3053
|
-
|
|
3944
|
+
mediaService.scheduleDownloadedMediaCleanup(downloadedMedia.map((mediaItem) => mediaItem.path));
|
|
3054
3945
|
}
|
|
3055
3946
|
};
|
|
3056
3947
|
const directTextCoalescer = new BufferedDirectMessageCoalescer({
|
|
@@ -3203,6 +4094,15 @@ export const bitrix24Plugin = {
|
|
|
3203
4094
|
messageId: msgCtx.messageId,
|
|
3204
4095
|
textLen: msgCtx.text.length,
|
|
3205
4096
|
});
|
|
4097
|
+
if (hasRecentOutboundMessage(msgCtx.messageId, ctx.accountId)) {
|
|
4098
|
+
logger.debug('Skipping recent outbound Bitrix24 message echo', {
|
|
4099
|
+
senderId: msgCtx.senderId,
|
|
4100
|
+
chatId: msgCtx.chatId,
|
|
4101
|
+
messageId: msgCtx.messageId,
|
|
4102
|
+
eventScope: msgCtx.eventScope,
|
|
4103
|
+
});
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
3206
4106
|
const pendingForwardContext = msgCtx.isForwarded
|
|
3207
4107
|
? directTextCoalescer.take(ctx.accountId, msgCtx.chatId)
|
|
3208
4108
|
: null;
|
|
@@ -3227,7 +4127,7 @@ export const bitrix24Plugin = {
|
|
|
3227
4127
|
chatId: msgCtx.chatInternalId,
|
|
3228
4128
|
})
|
|
3229
4129
|
: null;
|
|
3230
|
-
const agentWatchRules = config.agentMode
|
|
4130
|
+
const agentWatchRules = config.agentMode
|
|
3231
4131
|
? resolveAgentWatchRules({
|
|
3232
4132
|
config,
|
|
3233
4133
|
dialogId: msgCtx.chatId,
|
|
@@ -3237,37 +4137,61 @@ export const bitrix24Plugin = {
|
|
|
3237
4137
|
const watchRule = msgCtx.isGroup && groupAccess?.groupAllowed
|
|
3238
4138
|
? findMatchingWatchRule(msgCtx, groupAccess?.watch)
|
|
3239
4139
|
: undefined;
|
|
3240
|
-
const
|
|
4140
|
+
const botId = String(bot.botId);
|
|
4141
|
+
const ownerBotDmParticipantIds = new Set([msgCtx.senderId, msgCtx.chatId, msgCtx.chatInternalId]
|
|
4142
|
+
.map((value) => String(value ?? '').trim())
|
|
4143
|
+
.filter(Boolean));
|
|
4144
|
+
const isOwnerBotUserDmMirror = Boolean(config.agentMode
|
|
4145
|
+
&& msgCtx.eventScope === 'user'
|
|
4146
|
+
&& msgCtx.isDm
|
|
3241
4147
|
&& webhookOwnerId
|
|
3242
|
-
&&
|
|
4148
|
+
&& ownerBotDmParticipantIds.has(webhookOwnerId)
|
|
4149
|
+
&& ownerBotDmParticipantIds.has(botId));
|
|
4150
|
+
const isOwnerAuthoredMessage = Boolean(webhookOwnerId && msgCtx.senderId === webhookOwnerId);
|
|
4151
|
+
const isCurrentBotAuthoredMessage = msgCtx.senderId === botId;
|
|
4152
|
+
const activeWatchRule = watchRule?.mode === 'notifyOwnerDm'
|
|
4153
|
+
&& (isOwnerAuthoredMessage || isCurrentBotAuthoredMessage)
|
|
3243
4154
|
? undefined
|
|
3244
4155
|
: watchRule;
|
|
3245
|
-
const agentWatchRule = msgCtx
|
|
3246
|
-
|
|
4156
|
+
const agentWatchRule = findMatchingWatchRule(msgCtx, agentWatchRules);
|
|
4157
|
+
const activeAgentWatchRule = agentWatchRule?.mode === 'notifyOwnerDm'
|
|
4158
|
+
&& !isOwnerBotUserDmMirror
|
|
4159
|
+
&& !isOwnerAuthoredMessage
|
|
4160
|
+
&& !isCurrentBotAuthoredMessage
|
|
4161
|
+
? agentWatchRule
|
|
3247
4162
|
: undefined;
|
|
3248
4163
|
/** Shorthand: record message in RAM history for this dialog. */
|
|
3249
4164
|
const recordHistory = (body) => appendMessageToHistory({ historyCache, historyKey, historyLimit, msgCtx, body });
|
|
4165
|
+
if (isOwnerBotUserDmMirror) {
|
|
4166
|
+
logger.debug('Skipping mirrored agent-mode owner DM user event; bot channel will handle it', {
|
|
4167
|
+
senderId: msgCtx.senderId,
|
|
4168
|
+
chatId: msgCtx.chatId,
|
|
4169
|
+
messageId: msgCtx.messageId,
|
|
4170
|
+
eventScope: msgCtx.eventScope,
|
|
4171
|
+
webhookOwnerId,
|
|
4172
|
+
botId,
|
|
4173
|
+
});
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
3250
4176
|
if (msgCtx.eventScope === 'user') {
|
|
3251
|
-
const isBotDialogUserEvent = msgCtx.isDm && msgCtx.chatId === String(bot.botId);
|
|
3252
|
-
const isBotAuthoredUserEvent = msgCtx.senderId === String(bot.botId);
|
|
3253
|
-
if (isBotDialogUserEvent || isBotAuthoredUserEvent) {
|
|
3254
|
-
logger.debug('Skipping agent-mode user event for bot-owned conversation', {
|
|
3255
|
-
senderId: msgCtx.senderId,
|
|
3256
|
-
chatId: msgCtx.chatId,
|
|
3257
|
-
messageId: msgCtx.messageId,
|
|
3258
|
-
isBotDialogUserEvent,
|
|
3259
|
-
isBotAuthoredUserEvent,
|
|
3260
|
-
});
|
|
3261
|
-
return;
|
|
3262
|
-
}
|
|
3263
4177
|
recordHistory();
|
|
3264
|
-
if (
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
4178
|
+
if (activeAgentWatchRule) {
|
|
4179
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
4180
|
+
logger.debug('Skipping duplicate agent watch notification for recent message', {
|
|
4181
|
+
senderId: msgCtx.senderId,
|
|
4182
|
+
chatId: msgCtx.chatId,
|
|
4183
|
+
messageId: msgCtx.messageId,
|
|
4184
|
+
eventScope: msgCtx.eventScope,
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeAgentWatchRule)) {
|
|
4188
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
4189
|
+
logger.debug('User-event watch matched and notified webhook owner in DM', {
|
|
4190
|
+
senderId: msgCtx.senderId,
|
|
4191
|
+
chatId: msgCtx.chatId,
|
|
4192
|
+
messageId: msgCtx.messageId,
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
3271
4195
|
}
|
|
3272
4196
|
return;
|
|
3273
4197
|
}
|
|
@@ -3288,6 +4212,7 @@ export const bitrix24Plugin = {
|
|
|
3288
4212
|
await sendService.markRead(sendCtx, toMessageId(msgCtx.messageId));
|
|
3289
4213
|
if (msgCtx.isGroup
|
|
3290
4214
|
&& !activeWatchRule
|
|
4215
|
+
&& !activeAgentWatchRule
|
|
3291
4216
|
&& groupAccess?.requireMention
|
|
3292
4217
|
&& !msgCtx.wasMentioned) {
|
|
3293
4218
|
recordHistory();
|
|
@@ -3300,12 +4225,42 @@ export const bitrix24Plugin = {
|
|
|
3300
4225
|
}
|
|
3301
4226
|
if (activeWatchRule?.mode === 'notifyOwnerDm') {
|
|
3302
4227
|
recordHistory();
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
4228
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
4229
|
+
logger.debug('Skipping duplicate group watch notification for recent message', {
|
|
4230
|
+
senderId: msgCtx.senderId,
|
|
4231
|
+
chatId: msgCtx.chatId,
|
|
4232
|
+
messageId: msgCtx.messageId,
|
|
4233
|
+
eventScope: msgCtx.eventScope,
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeWatchRule)) {
|
|
4237
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
4238
|
+
logger.debug('Group watch matched and notified webhook owner in DM', {
|
|
4239
|
+
senderId: msgCtx.senderId,
|
|
4240
|
+
chatId: msgCtx.chatId,
|
|
4241
|
+
messageId: msgCtx.messageId,
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
if (activeAgentWatchRule) {
|
|
4247
|
+
recordHistory();
|
|
4248
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
4249
|
+
logger.debug('Skipping duplicate bot-scope agent watch notification for recent message', {
|
|
4250
|
+
senderId: msgCtx.senderId,
|
|
4251
|
+
chatId: msgCtx.chatId,
|
|
4252
|
+
messageId: msgCtx.messageId,
|
|
4253
|
+
eventScope: msgCtx.eventScope,
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4256
|
+
else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeAgentWatchRule)) {
|
|
4257
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
4258
|
+
logger.debug('Bot-scope agent watch matched and notified webhook owner in DM', {
|
|
4259
|
+
senderId: msgCtx.senderId,
|
|
4260
|
+
chatId: msgCtx.chatId,
|
|
4261
|
+
messageId: msgCtx.messageId,
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
3309
4264
|
return;
|
|
3310
4265
|
}
|
|
3311
4266
|
const accessResult = activeWatchRule
|
|
@@ -3537,9 +4492,14 @@ export const bitrix24Plugin = {
|
|
|
3537
4492
|
accountId: ctx.accountId,
|
|
3538
4493
|
peer: conversation.peer,
|
|
3539
4494
|
});
|
|
4495
|
+
const inlineButtonsAgentHint = buildBitrix24InlineButtonsAgentHint();
|
|
3540
4496
|
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
3541
|
-
const
|
|
4497
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ isDm });
|
|
4498
|
+
const nativeReplyAgentHint = canUseNativeReply
|
|
4499
|
+
? buildBitrix24NativeReplyAgentHint(commandMessageId)
|
|
4500
|
+
: undefined;
|
|
3542
4501
|
const commandBodyForAgent = [
|
|
4502
|
+
inlineButtonsAgentHint,
|
|
3543
4503
|
fileDeliveryAgentHint,
|
|
3544
4504
|
nativeReplyAgentHint,
|
|
3545
4505
|
commandText,
|
|
@@ -3601,10 +4561,12 @@ export const bitrix24Plugin = {
|
|
|
3601
4561
|
const sendOptions = {
|
|
3602
4562
|
keyboard,
|
|
3603
4563
|
convertMarkdown: formattedPayload.convertMarkdown,
|
|
3604
|
-
...(
|
|
4564
|
+
...(canUseNativeReply && replyDirective.replyToMessageId
|
|
4565
|
+
? { replyToMessageId: replyDirective.replyToMessageId }
|
|
4566
|
+
: {}),
|
|
3605
4567
|
};
|
|
3606
4568
|
if (!commandReplyDelivered) {
|
|
3607
|
-
if (isDm || replyDirective.replyToMessageId) {
|
|
4569
|
+
if (isDm || (canUseNativeReply && replyDirective.replyToMessageId)) {
|
|
3608
4570
|
commandReplyDelivered = true;
|
|
3609
4571
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
3610
4572
|
...sendOptions,
|
|
@@ -3631,20 +4593,68 @@ export const bitrix24Plugin = {
|
|
|
3631
4593
|
},
|
|
3632
4594
|
},
|
|
3633
4595
|
});
|
|
4596
|
+
if (!commandReplyDelivered && hasRecentMediaDelivery(sendCtx.dialogId, commandMessageId, ctx.accountId)) {
|
|
4597
|
+
commandReplyDelivered = true;
|
|
4598
|
+
logger.debug('Suppressing command fallback after successful Bitrix24 file upload in the same turn', {
|
|
4599
|
+
commandName,
|
|
4600
|
+
senderId,
|
|
4601
|
+
dialogId,
|
|
4602
|
+
messageId: commandMessageId,
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
if (!commandReplyDelivered && hasRecentSuccessfulAction(commandMessageId, ctx.accountId)) {
|
|
4606
|
+
commandReplyDelivered = true;
|
|
4607
|
+
logger.debug('Suppressing command fallback after successful Bitrix24 action in the same turn', {
|
|
4608
|
+
commandName,
|
|
4609
|
+
senderId,
|
|
4610
|
+
dialogId,
|
|
4611
|
+
messageId: commandMessageId,
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
const shouldWaitForEmptyCommandReplyFallback = !commandReplyDelivered
|
|
4615
|
+
&& isEmptyDispatchResult(dispatchResult);
|
|
3634
4616
|
if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3635
4617
|
logger.debug('Command reply completed without queued final block, waiting before fallback', {
|
|
3636
4618
|
commandName,
|
|
3637
4619
|
senderId,
|
|
3638
4620
|
dialogId,
|
|
3639
4621
|
counts: dispatchResult.counts,
|
|
4622
|
+
fallbackDelayMs: shouldWaitForEmptyCommandReplyFallback
|
|
4623
|
+
? EMPTY_REPLY_FALLBACK_GRACE_MS
|
|
4624
|
+
: REPLY_FALLBACK_GRACE_MS,
|
|
3640
4625
|
});
|
|
3641
|
-
|
|
4626
|
+
if (shouldWaitForEmptyCommandReplyFallback) {
|
|
4627
|
+
await waitEmptyReplyFallbackGraceWindow();
|
|
4628
|
+
}
|
|
4629
|
+
else {
|
|
4630
|
+
await waitReplyFallbackGraceWindow();
|
|
4631
|
+
}
|
|
3642
4632
|
}
|
|
3643
|
-
if (!commandReplyDelivered && dispatchResult
|
|
4633
|
+
if (!commandReplyDelivered && isEmptyDispatchResult(dispatchResult)) {
|
|
4634
|
+
commandReplyDelivered = true;
|
|
4635
|
+
logger.warn('Command reply completed without any user-visible payload or tool activity; sending fallback notice', {
|
|
4636
|
+
commandName,
|
|
4637
|
+
senderId,
|
|
4638
|
+
dialogId,
|
|
4639
|
+
messageId: commandMessageId,
|
|
4640
|
+
counts: dispatchResult.counts,
|
|
4641
|
+
});
|
|
4642
|
+
const fallbackText = emptyReplyFallback(cmdCtx.language);
|
|
4643
|
+
if (isDm) {
|
|
4644
|
+
await sendService.sendText(sendCtx, fallbackText);
|
|
4645
|
+
}
|
|
4646
|
+
else {
|
|
4647
|
+
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
4648
|
+
keyboard: defaultCommandKeyboard,
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
else if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3644
4653
|
logger.warn('Command reply completed without a final user-visible message; fallback notice suppressed', {
|
|
3645
4654
|
commandName,
|
|
3646
4655
|
senderId,
|
|
3647
4656
|
dialogId,
|
|
4657
|
+
messageId: commandMessageId,
|
|
3648
4658
|
counts: dispatchResult.counts,
|
|
3649
4659
|
});
|
|
3650
4660
|
}
|
|
@@ -3653,6 +4663,7 @@ export const bitrix24Plugin = {
|
|
|
3653
4663
|
commandName,
|
|
3654
4664
|
senderId,
|
|
3655
4665
|
dialogId,
|
|
4666
|
+
messageId: commandMessageId,
|
|
3656
4667
|
});
|
|
3657
4668
|
}
|
|
3658
4669
|
}
|