@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/src/channel.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
accessApproved,
|
|
40
40
|
accessDenied,
|
|
41
41
|
commandKeyboardLabels,
|
|
42
|
+
emptyReplyFallback,
|
|
42
43
|
groupPairingPending,
|
|
43
44
|
mediaDownloadFailed,
|
|
44
45
|
groupChatUnsupported,
|
|
@@ -57,6 +58,12 @@ import type {
|
|
|
57
58
|
B24Attach,
|
|
58
59
|
B24AttachBlock,
|
|
59
60
|
B24AttachColorToken,
|
|
61
|
+
B24AttachFileItem,
|
|
62
|
+
B24AttachGridDisplay,
|
|
63
|
+
B24AttachGridItem,
|
|
64
|
+
B24AttachImageItem,
|
|
65
|
+
B24AttachLinkValue,
|
|
66
|
+
B24AttachUserValue,
|
|
60
67
|
B24MsgContext,
|
|
61
68
|
B24InputActionStatusCode,
|
|
62
69
|
B24V2FetchEventItem,
|
|
@@ -85,6 +92,7 @@ const THINKING_STATUS_REFRESH_GRACE_MS = 6000;
|
|
|
85
92
|
const DIRECT_TEXT_COALESCE_DEBOUNCE_MS = 200;
|
|
86
93
|
const DIRECT_TEXT_COALESCE_MAX_WAIT_MS = 5000;
|
|
87
94
|
const REPLY_FALLBACK_GRACE_MS = 750;
|
|
95
|
+
const EMPTY_REPLY_FALLBACK_GRACE_MS = 15 * 1000;
|
|
88
96
|
const ACCESS_DENIED_NOTICE_COOLDOWN_MS = 60000;
|
|
89
97
|
const AUTO_BOT_CODE_MAX_CANDIDATES = 100;
|
|
90
98
|
const MEDIA_DOWNLOAD_CONCURRENCY = 2;
|
|
@@ -101,11 +109,26 @@ const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
|
|
|
101
109
|
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;
|
|
102
110
|
const NATIVE_FORWARD_ACK_TTL_MS = 60 * 1000;
|
|
103
111
|
const NATIVE_REACTION_ACK_TTL_MS = 60 * 1000;
|
|
112
|
+
const NATIVE_FORWARD_SUCCESS_REACTION = 'whiteHeavyCheckMark';
|
|
113
|
+
const REACTION_TYPING_RESET_DURATION_SECONDS = 1;
|
|
114
|
+
const GENERIC_OUTBOUND_FILE_NAME_RE = /^(translated?|translation|output|result|file|document)(?:[_-][a-z]{2,8})?$/i;
|
|
115
|
+
const GENERIC_INBOUND_FILE_NAME_RE = /^(?:file[_-]\d+|[0-9a-f]{8,}(?:-[0-9a-f]{4,})*(?:_file[_-]?\d+)?)$/i;
|
|
116
|
+
const RECENT_MEDIA_DELIVERY_TTL_MS = 60 * 1000;
|
|
117
|
+
const RECENT_SUCCESSFUL_ACTION_TTL_MS = 60 * 1000;
|
|
118
|
+
const RECENT_OUTBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
119
|
+
const RECENT_WATCH_NOTIFICATION_TTL_MS = 30 * 60 * 1000;
|
|
104
120
|
const RECENT_INBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
105
121
|
const RECENT_INBOUND_MESSAGE_LIMIT = 20;
|
|
106
122
|
const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
|
|
107
123
|
const pendingNativeForwardAcks = new Map<string, number>();
|
|
108
124
|
const pendingNativeReactionAcks = new Map<string, { emoji: string; expiresAt: number }>();
|
|
125
|
+
const recentSuccessfulMediaDeliveries = new Map<string, {
|
|
126
|
+
expiresAt: number;
|
|
127
|
+
mediaByName: Map<string, string>;
|
|
128
|
+
}>();
|
|
129
|
+
const recentSuccessfulActionsByMessage = new Map<string, number>();
|
|
130
|
+
const recentOutboundMessagesById = new Map<string, number>();
|
|
131
|
+
const recentWatchNotificationsByMessage = new Map<string, number>();
|
|
109
132
|
const recentInboundMessagesByDialog = new Map<string, Array<{
|
|
110
133
|
messageId: string;
|
|
111
134
|
body: string;
|
|
@@ -115,6 +138,7 @@ const inboundMessageContextById = new Map<string, {
|
|
|
115
138
|
dialogId: string;
|
|
116
139
|
dialogKey: string;
|
|
117
140
|
body: string;
|
|
141
|
+
mediaNames: string[];
|
|
118
142
|
timestamp: number;
|
|
119
143
|
}>();
|
|
120
144
|
|
|
@@ -294,6 +318,245 @@ function resolvePendingNativeForwardAckDialogId(
|
|
|
294
318
|
return inboundMessageContextById.get(messageKey)?.dialogId ?? fallbackDialogId;
|
|
295
319
|
}
|
|
296
320
|
|
|
321
|
+
function resolveRecentInboundDialogId(
|
|
322
|
+
currentMessageId: string | number | undefined,
|
|
323
|
+
accountId?: string,
|
|
324
|
+
): string | undefined {
|
|
325
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
326
|
+
if (!messageKey) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
pruneRecentInboundMessages();
|
|
331
|
+
return inboundMessageContextById.get(messageKey)?.dialogId;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function splitFileNameParts(fileName: string): { stem: string; extension: string } {
|
|
335
|
+
const normalizedFileName = basename(fileName).trim();
|
|
336
|
+
const lastDotIndex = normalizedFileName.lastIndexOf('.');
|
|
337
|
+
if (lastDotIndex <= 0) {
|
|
338
|
+
return { stem: normalizedFileName, extension: '' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
stem: normalizedFileName.slice(0, lastDotIndex).trim(),
|
|
343
|
+
extension: normalizedFileName.slice(lastDotIndex).trim(),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function normalizeSourceFileStem(stem: string): string {
|
|
348
|
+
return stem
|
|
349
|
+
.trim()
|
|
350
|
+
.replace(/\s+\(\d+\)$/u, '')
|
|
351
|
+
.trim();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function isGenericOutboundFileName(fileName: string): boolean {
|
|
355
|
+
const { stem } = splitFileNameParts(fileName);
|
|
356
|
+
return GENERIC_OUTBOUND_FILE_NAME_RE.test(stem);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isSemanticInboundFileName(fileName: string): boolean {
|
|
360
|
+
const { stem } = splitFileNameParts(fileName);
|
|
361
|
+
return Boolean(stem) && !GENERIC_INBOUND_FILE_NAME_RE.test(stem);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function inferTranslationSuffixFromBody(body: string): string | undefined {
|
|
365
|
+
const normalizedBody = body.toLowerCase();
|
|
366
|
+
if (!/translate|translation|перевед|перевод/u.test(normalizedBody)) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const languagePatterns: Array<[string, RegExp]> = [
|
|
371
|
+
['en', /english|английск/u],
|
|
372
|
+
['ru', /russian|русск/u],
|
|
373
|
+
['de', /german|немец/u],
|
|
374
|
+
['fr', /french|француз/u],
|
|
375
|
+
['es', /spanish|испан/u],
|
|
376
|
+
['it', /italian|итальян/u],
|
|
377
|
+
['pt', /portuguese|португал/u],
|
|
378
|
+
['zh', /chinese|китай/u],
|
|
379
|
+
['ja', /japanese|япон/u],
|
|
380
|
+
['ko', /korean|корей/u],
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
for (const [code, pattern] of languagePatterns) {
|
|
384
|
+
if (pattern.test(normalizedBody)) {
|
|
385
|
+
return `_${code}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return '_translated';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function inferTranslationSuffix(requestedFileName: string, body: string): string | undefined {
|
|
393
|
+
const { stem } = splitFileNameParts(requestedFileName);
|
|
394
|
+
const suffixMatch = stem.match(/(?:^|[_-])(en|ru|de|fr|es|it|pt|zh|ja|ko)$/i);
|
|
395
|
+
if (suffixMatch?.[1]) {
|
|
396
|
+
return `_${suffixMatch[1].toLowerCase()}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return inferTranslationSuffixFromBody(body);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function resolveRecentInboundContext(params: {
|
|
403
|
+
dialogId: string;
|
|
404
|
+
currentMessageId?: string | number;
|
|
405
|
+
accountId?: string;
|
|
406
|
+
}): {
|
|
407
|
+
body: string;
|
|
408
|
+
mediaNames: string[];
|
|
409
|
+
} | undefined {
|
|
410
|
+
pruneRecentInboundMessages();
|
|
411
|
+
|
|
412
|
+
const explicitKey = buildAccountMessageScopeKey(params.accountId, params.currentMessageId);
|
|
413
|
+
const explicitEntry = explicitKey ? inboundMessageContextById.get(explicitKey) : undefined;
|
|
414
|
+
if (explicitEntry) {
|
|
415
|
+
return explicitEntry;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const dialogKey = buildAccountDialogScopeKey(params.accountId, params.dialogId);
|
|
419
|
+
if (!dialogKey) {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const recentEntries = recentInboundMessagesByDialog.get(dialogKey);
|
|
424
|
+
if (!recentEntries?.length) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (let index = recentEntries.length - 1; index >= 0; index -= 1) {
|
|
429
|
+
const entry = recentEntries[index];
|
|
430
|
+
const messageKey = buildAccountMessageScopeKey(params.accountId, entry.messageId);
|
|
431
|
+
if (!messageKey) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const inboundEntry = inboundMessageContextById.get(messageKey);
|
|
436
|
+
if (inboundEntry) {
|
|
437
|
+
return inboundEntry;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function resolveSemanticOutboundFileName(params: {
|
|
445
|
+
requestedFileName: string;
|
|
446
|
+
dialogId: string;
|
|
447
|
+
currentMessageId?: string | number;
|
|
448
|
+
accountId?: string;
|
|
449
|
+
}): string {
|
|
450
|
+
const requestedBaseName = basename(params.requestedFileName).trim();
|
|
451
|
+
if (!requestedBaseName || !isGenericOutboundFileName(requestedBaseName)) {
|
|
452
|
+
return requestedBaseName;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const inboundContext = resolveRecentInboundContext({
|
|
456
|
+
dialogId: params.dialogId,
|
|
457
|
+
currentMessageId: params.currentMessageId,
|
|
458
|
+
accountId: params.accountId,
|
|
459
|
+
});
|
|
460
|
+
if (!inboundContext || inboundContext.mediaNames.length !== 1) {
|
|
461
|
+
return requestedBaseName;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const sourceFileName = inboundContext.mediaNames[0];
|
|
465
|
+
if (!isSemanticInboundFileName(sourceFileName)) {
|
|
466
|
+
return requestedBaseName;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const { stem: sourceStemRaw, extension: sourceExtension } = splitFileNameParts(sourceFileName);
|
|
470
|
+
const { extension: requestedExtension } = splitFileNameParts(requestedBaseName);
|
|
471
|
+
const normalizedSourceStem = normalizeSourceFileStem(sourceStemRaw);
|
|
472
|
+
if (!normalizedSourceStem) {
|
|
473
|
+
return requestedBaseName;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const suffix = inferTranslationSuffix(requestedBaseName, inboundContext.body) ?? '';
|
|
477
|
+
const nextStem = suffix && !normalizedSourceStem.endsWith(suffix)
|
|
478
|
+
? `${normalizedSourceStem}${suffix}`
|
|
479
|
+
: normalizedSourceStem;
|
|
480
|
+
const nextExtension = requestedExtension || sourceExtension;
|
|
481
|
+
|
|
482
|
+
return nextExtension ? `${nextStem}${nextExtension}` : nextStem;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function maybeSendReactionTypingReset(params: {
|
|
486
|
+
sendService: unknown;
|
|
487
|
+
webhookUrl: string;
|
|
488
|
+
bot: BotContext;
|
|
489
|
+
dialogId: string | undefined;
|
|
490
|
+
}): Promise<void> {
|
|
491
|
+
const dialogId = typeof params.dialogId === 'string' ? params.dialogId.trim() : '';
|
|
492
|
+
if (!dialogId) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const sendTyping = (params.sendService as { sendTyping?: unknown }).sendTyping;
|
|
497
|
+
if (typeof sendTyping !== 'function') {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
await sendTyping.call(
|
|
503
|
+
params.sendService,
|
|
504
|
+
{ webhookUrl: params.webhookUrl, bot: params.bot, dialogId },
|
|
505
|
+
REACTION_TYPING_RESET_DURATION_SECONDS,
|
|
506
|
+
);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
defaultLogger.debug('Failed to send Bitrix24 reaction typing reset', {
|
|
509
|
+
dialogId,
|
|
510
|
+
error: error instanceof Error ? error.message : String(error),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function maybeAddNativeForwardSuccessReaction(params: {
|
|
516
|
+
api: unknown;
|
|
517
|
+
sendService: unknown;
|
|
518
|
+
webhookUrl: string;
|
|
519
|
+
bot: BotContext;
|
|
520
|
+
dialogId: string | undefined;
|
|
521
|
+
currentMessageId: string | number | undefined;
|
|
522
|
+
}): Promise<void> {
|
|
523
|
+
const messageId = toMessageId(params.currentMessageId);
|
|
524
|
+
if (!messageId) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const addReaction = (params.api as { addReaction?: unknown }).addReaction;
|
|
529
|
+
if (typeof addReaction !== 'function') {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
let shouldResetTyping = false;
|
|
534
|
+
try {
|
|
535
|
+
await addReaction.call(params.api, params.webhookUrl, params.bot, messageId, NATIVE_FORWARD_SUCCESS_REACTION);
|
|
536
|
+
shouldResetTyping = true;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
539
|
+
if (errorMessage.includes('REACTION_ALREADY_SET')) {
|
|
540
|
+
defaultLogger.debug('Native forward success reaction already set', { messageId });
|
|
541
|
+
shouldResetTyping = true;
|
|
542
|
+
} else {
|
|
543
|
+
defaultLogger.warn('Failed to add native forward success reaction', {
|
|
544
|
+
messageId,
|
|
545
|
+
error: errorMessage,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (shouldResetTyping) {
|
|
551
|
+
await maybeSendReactionTypingReset({
|
|
552
|
+
sendService: params.sendService,
|
|
553
|
+
webhookUrl: params.webhookUrl,
|
|
554
|
+
bot: params.bot,
|
|
555
|
+
dialogId: params.dialogId,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
297
560
|
function buildPendingNativeReactionAckKey(
|
|
298
561
|
currentMessageId: string | number | undefined,
|
|
299
562
|
accountId?: string,
|
|
@@ -301,6 +564,211 @@ function buildPendingNativeReactionAckKey(
|
|
|
301
564
|
return buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
302
565
|
}
|
|
303
566
|
|
|
567
|
+
function pruneRecentSuccessfulActions(now = Date.now()): void {
|
|
568
|
+
for (const [key, expiresAt] of recentSuccessfulActionsByMessage.entries()) {
|
|
569
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
570
|
+
recentSuccessfulActionsByMessage.delete(key);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function markRecentSuccessfulAction(
|
|
576
|
+
currentMessageId: string | number | undefined,
|
|
577
|
+
accountId?: string,
|
|
578
|
+
): void {
|
|
579
|
+
const key = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
580
|
+
if (!key) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
pruneRecentSuccessfulActions();
|
|
585
|
+
recentSuccessfulActionsByMessage.set(key, Date.now() + RECENT_SUCCESSFUL_ACTION_TTL_MS);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function hasRecentSuccessfulAction(
|
|
589
|
+
currentMessageId: string | number | undefined,
|
|
590
|
+
accountId?: string,
|
|
591
|
+
): boolean {
|
|
592
|
+
const key = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
593
|
+
if (!key) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
pruneRecentSuccessfulActions();
|
|
598
|
+
return recentSuccessfulActionsByMessage.has(key);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function pruneRecentOutboundMessages(now = Date.now()): void {
|
|
602
|
+
for (const [key, expiresAt] of recentOutboundMessagesById.entries()) {
|
|
603
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
604
|
+
recentOutboundMessagesById.delete(key);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function markRecentOutboundMessage(
|
|
610
|
+
messageId: string | number | undefined,
|
|
611
|
+
accountId?: string,
|
|
612
|
+
): void {
|
|
613
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
614
|
+
if (!key) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
pruneRecentOutboundMessages();
|
|
619
|
+
recentOutboundMessagesById.set(key, Date.now() + RECENT_OUTBOUND_MESSAGE_TTL_MS);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function hasRecentOutboundMessage(
|
|
623
|
+
messageId: string | number | undefined,
|
|
624
|
+
accountId?: string,
|
|
625
|
+
): boolean {
|
|
626
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
627
|
+
if (!key) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
pruneRecentOutboundMessages();
|
|
632
|
+
return recentOutboundMessagesById.has(key);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function pruneRecentWatchNotifications(now = Date.now()): void {
|
|
636
|
+
for (const [key, expiresAt] of recentWatchNotificationsByMessage.entries()) {
|
|
637
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
638
|
+
recentWatchNotificationsByMessage.delete(key);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function markRecentWatchNotification(
|
|
644
|
+
messageId: string | number | undefined,
|
|
645
|
+
accountId?: string,
|
|
646
|
+
): void {
|
|
647
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
648
|
+
if (!key) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
pruneRecentWatchNotifications();
|
|
653
|
+
recentWatchNotificationsByMessage.set(key, Date.now() + RECENT_WATCH_NOTIFICATION_TTL_MS);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function hasRecentWatchNotification(
|
|
657
|
+
messageId: string | number | undefined,
|
|
658
|
+
accountId?: string,
|
|
659
|
+
): boolean {
|
|
660
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
661
|
+
if (!key) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
pruneRecentWatchNotifications();
|
|
666
|
+
return recentWatchNotificationsByMessage.has(key);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function buildRecentMediaDeliveryKey(
|
|
670
|
+
dialogId: string,
|
|
671
|
+
currentMessageId: string | number | undefined,
|
|
672
|
+
accountId?: string,
|
|
673
|
+
): string | undefined {
|
|
674
|
+
return buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function normalizeMediaDeliveryName(mediaUrl: string): string {
|
|
678
|
+
return basename(mediaUrl.trim()).toLowerCase();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function pruneRecentMediaDeliveries(now = Date.now()): void {
|
|
682
|
+
for (const [key, entry] of recentSuccessfulMediaDeliveries.entries()) {
|
|
683
|
+
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
684
|
+
recentSuccessfulMediaDeliveries.delete(key);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function markRecentMediaDelivery(params: {
|
|
690
|
+
dialogId: string;
|
|
691
|
+
currentMessageId: string | number | undefined;
|
|
692
|
+
mediaUrls: string[];
|
|
693
|
+
accountId?: string;
|
|
694
|
+
messageId?: string;
|
|
695
|
+
}): void {
|
|
696
|
+
const key = buildRecentMediaDeliveryKey(params.dialogId, params.currentMessageId, params.accountId);
|
|
697
|
+
if (!key || params.mediaUrls.length === 0) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
pruneRecentMediaDeliveries();
|
|
702
|
+
const mediaByName = recentSuccessfulMediaDeliveries.get(key)?.mediaByName ?? new Map<string, string>();
|
|
703
|
+
const normalizedMessageId = String(params.messageId ?? '').trim();
|
|
704
|
+
|
|
705
|
+
for (const mediaUrl of params.mediaUrls) {
|
|
706
|
+
const mediaName = normalizeMediaDeliveryName(mediaUrl);
|
|
707
|
+
if (!mediaName) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
mediaByName.set(mediaName, normalizedMessageId);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
recentSuccessfulMediaDeliveries.set(key, {
|
|
715
|
+
expiresAt: Date.now() + RECENT_MEDIA_DELIVERY_TTL_MS,
|
|
716
|
+
mediaByName,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function findRecentMediaDelivery(params: {
|
|
721
|
+
dialogId: string;
|
|
722
|
+
currentMessageId: string | number | undefined;
|
|
723
|
+
mediaUrls: string[];
|
|
724
|
+
accountId?: string;
|
|
725
|
+
}): { messageId?: string } | null {
|
|
726
|
+
const key = buildRecentMediaDeliveryKey(params.dialogId, params.currentMessageId, params.accountId);
|
|
727
|
+
if (!key || params.mediaUrls.length === 0) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
pruneRecentMediaDeliveries();
|
|
732
|
+
const entry = recentSuccessfulMediaDeliveries.get(key);
|
|
733
|
+
if (!entry) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const normalizedNames = params.mediaUrls
|
|
738
|
+
.map((mediaUrl) => normalizeMediaDeliveryName(mediaUrl))
|
|
739
|
+
.filter(Boolean);
|
|
740
|
+
if (normalizedNames.length === 0) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
for (const mediaName of normalizedNames) {
|
|
745
|
+
if (!entry.mediaByName.has(mediaName)) {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const lastMessageId = normalizedNames
|
|
751
|
+
.map((mediaName) => entry.mediaByName.get(mediaName) ?? '')
|
|
752
|
+
.find(Boolean);
|
|
753
|
+
|
|
754
|
+
return lastMessageId ? { messageId: lastMessageId } : {};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function hasRecentMediaDelivery(
|
|
758
|
+
dialogId: string,
|
|
759
|
+
currentMessageId: string | number | undefined,
|
|
760
|
+
accountId?: string,
|
|
761
|
+
): boolean {
|
|
762
|
+
const key = buildRecentMediaDeliveryKey(dialogId, currentMessageId, accountId);
|
|
763
|
+
if (!key) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
pruneRecentMediaDeliveries();
|
|
768
|
+
const entry = recentSuccessfulMediaDeliveries.get(key);
|
|
769
|
+
return Boolean(entry && entry.mediaByName.size > 0);
|
|
770
|
+
}
|
|
771
|
+
|
|
304
772
|
function prunePendingNativeReactionAcks(now = Date.now()): void {
|
|
305
773
|
for (const [key, entry] of pendingNativeReactionAcks.entries()) {
|
|
306
774
|
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
@@ -372,6 +840,7 @@ function rememberRecentInboundMessage(
|
|
|
372
840
|
dialogId: string,
|
|
373
841
|
messageId: string | number | undefined,
|
|
374
842
|
body: string,
|
|
843
|
+
mediaNames: string[] = [],
|
|
375
844
|
timestamp = Date.now(),
|
|
376
845
|
accountId?: string,
|
|
377
846
|
): void {
|
|
@@ -397,6 +866,10 @@ function rememberRecentInboundMessage(
|
|
|
397
866
|
dialogId,
|
|
398
867
|
dialogKey,
|
|
399
868
|
body: normalizedBody,
|
|
869
|
+
mediaNames: mediaNames
|
|
870
|
+
.filter((name) => typeof name === 'string')
|
|
871
|
+
.map((name) => name.trim())
|
|
872
|
+
.filter(Boolean),
|
|
400
873
|
timestamp,
|
|
401
874
|
});
|
|
402
875
|
}
|
|
@@ -558,6 +1031,26 @@ function extractReplyDirective(params: {
|
|
|
558
1031
|
return { cleanText };
|
|
559
1032
|
}
|
|
560
1033
|
|
|
1034
|
+
function isBitrix24DirectDialogId(dialogId: string | undefined): boolean {
|
|
1035
|
+
const normalizedDialogId = String(dialogId ?? '').trim();
|
|
1036
|
+
return normalizedDialogId.length > 0 && !normalizedDialogId.startsWith('chat');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function shouldUseBitrix24NativeReply(params: {
|
|
1040
|
+
isDm?: boolean;
|
|
1041
|
+
dialogId?: string;
|
|
1042
|
+
}): boolean {
|
|
1043
|
+
if (typeof params.isDm === 'boolean') {
|
|
1044
|
+
return !params.isDm;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (!params.dialogId) {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return !isBitrix24DirectDialogId(params.dialogId);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
561
1054
|
function findNativeForwardTarget(params: {
|
|
562
1055
|
accountId?: string;
|
|
563
1056
|
requestText: string;
|
|
@@ -728,8 +1221,8 @@ function buildBitrix24NativeReplyAgentHint(currentMessageId: string | number | u
|
|
|
728
1221
|
return [
|
|
729
1222
|
'[Bitrix24 native reply instruction]',
|
|
730
1223
|
`Current Bitrix24 message id: ${normalizedMessageId}.`,
|
|
731
|
-
'
|
|
732
|
-
'For text-only payloads you can also prepend [[reply_to_current]] to the reply text.',
|
|
1224
|
+
'Only in group chats, if you intentionally want a native Bitrix24 reply to the current inbound message, set "replyToMessageId" to that id.',
|
|
1225
|
+
'For text-only payloads in group chats, you can also prepend [[reply_to_current]] to the reply text.',
|
|
733
1226
|
'Do not use a native reply unless you actually want reply threading.',
|
|
734
1227
|
'[/Bitrix24 native reply instruction]',
|
|
735
1228
|
].join('\n');
|
|
@@ -738,14 +1231,54 @@ function buildBitrix24NativeReplyAgentHint(currentMessageId: string | number | u
|
|
|
738
1231
|
function buildBitrix24FileDeliveryAgentHint(): string {
|
|
739
1232
|
return [
|
|
740
1233
|
'[Bitrix24 file delivery instruction]',
|
|
741
|
-
'When you need to deliver a real file or document to the user,
|
|
1234
|
+
'When you need to deliver a real file or document to the user, prefer a structured reply payload with field "mediaUrl" or "mediaUrls".',
|
|
742
1235
|
'Set "mediaUrl" to the local path of the generated file in the OpenClaw workspace or managed media directory.',
|
|
1236
|
+
'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.',
|
|
1237
|
+
'If read on an attached Bitrix24 media path fails, fall back to the inline <file> content from the current prompt instead of giving up.',
|
|
1238
|
+
'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.',
|
|
743
1239
|
'Use "text" only for an optional short caption.',
|
|
1240
|
+
'For file requests, follow this order exactly: read/prepare content, write the output file, send that file once, then stop.',
|
|
1241
|
+
'Never call the file-delivery tool before the write tool has succeeded for that file path.',
|
|
1242
|
+
'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.',
|
|
1243
|
+
'After one successful file upload in a turn, do not send the same file again and do not send an extra text-only confirmation.',
|
|
1244
|
+
'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.',
|
|
1245
|
+
'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.',
|
|
1246
|
+
'If the user asked for a file and you have not emitted the actual file delivery payload yet, do not answer with "NO_REPLY".',
|
|
744
1247
|
'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.',
|
|
745
1248
|
'[/Bitrix24 file delivery instruction]',
|
|
746
1249
|
].join('\n');
|
|
747
1250
|
}
|
|
748
1251
|
|
|
1252
|
+
export function buildBitrix24InlineButtonsAgentHint(): string {
|
|
1253
|
+
return [
|
|
1254
|
+
'[Bitrix24 buttons instruction]',
|
|
1255
|
+
'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.',
|
|
1256
|
+
'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"}]].',
|
|
1257
|
+
'The channel will strip that markup from the visible text and render native Bitrix24 buttons automatically.',
|
|
1258
|
+
'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.',
|
|
1259
|
+
'[/Bitrix24 buttons instruction]',
|
|
1260
|
+
].join('\n');
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function getDispatchCount(counts: Record<string, number> | undefined, key: 'tool' | 'block' | 'final'): number {
|
|
1264
|
+
const value = counts?.[key];
|
|
1265
|
+
return Number.isFinite(value) ? Number(value) : 0;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function isEmptyDispatchResult(
|
|
1269
|
+
dispatchResult: { queuedFinal: boolean; counts: Record<string, number> } | undefined,
|
|
1270
|
+
): boolean {
|
|
1271
|
+
if (!dispatchResult || dispatchResult.queuedFinal !== false) {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const toolCount = getDispatchCount(dispatchResult.counts, 'tool');
|
|
1276
|
+
const blockCount = getDispatchCount(dispatchResult.counts, 'block');
|
|
1277
|
+
const finalCount = getDispatchCount(dispatchResult.counts, 'final');
|
|
1278
|
+
|
|
1279
|
+
return toolCount + blockCount + finalCount === 0;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
749
1282
|
function readExplicitForwardMessageIds(rawParams: Record<string, unknown>): number[] {
|
|
750
1283
|
return parseActionMessageIds(
|
|
751
1284
|
rawParams.forwardMessageIds
|
|
@@ -991,6 +1524,10 @@ async function waitReplyFallbackGraceWindow(): Promise<void> {
|
|
|
991
1524
|
await new Promise((resolve) => setTimeout(resolve, REPLY_FALLBACK_GRACE_MS));
|
|
992
1525
|
}
|
|
993
1526
|
|
|
1527
|
+
async function waitEmptyReplyFallbackGraceWindow(): Promise<void> {
|
|
1528
|
+
await new Promise((resolve) => setTimeout(resolve, EMPTY_REPLY_FALLBACK_GRACE_MS));
|
|
1529
|
+
}
|
|
1530
|
+
|
|
994
1531
|
export function canCoalesceDirectMessage(
|
|
995
1532
|
msgCtx: B24MsgContext,
|
|
996
1533
|
config: Bitrix24AccountConfig,
|
|
@@ -1680,6 +2217,10 @@ export function __setGatewayStateForTests(state: GatewayState | null): void {
|
|
|
1680
2217
|
if (state === null) {
|
|
1681
2218
|
pendingNativeForwardAcks.clear();
|
|
1682
2219
|
pendingNativeReactionAcks.clear();
|
|
2220
|
+
recentSuccessfulMediaDeliveries.clear();
|
|
2221
|
+
recentSuccessfulActionsByMessage.clear();
|
|
2222
|
+
recentOutboundMessagesById.clear();
|
|
2223
|
+
recentWatchNotificationsByMessage.clear();
|
|
1683
2224
|
recentInboundMessagesByDialog.clear();
|
|
1684
2225
|
inboundMessageContextById.clear();
|
|
1685
2226
|
}
|
|
@@ -1690,12 +2231,14 @@ export function __rememberRecentInboundMessageForTests(params: {
|
|
|
1690
2231
|
dialogId: string;
|
|
1691
2232
|
messageId: string | number;
|
|
1692
2233
|
body: string;
|
|
2234
|
+
mediaNames?: string[];
|
|
1693
2235
|
timestamp?: number;
|
|
1694
2236
|
}): void {
|
|
1695
2237
|
rememberRecentInboundMessage(
|
|
1696
2238
|
params.dialogId,
|
|
1697
2239
|
params.messageId,
|
|
1698
2240
|
params.body,
|
|
2241
|
+
params.mediaNames,
|
|
1699
2242
|
params.timestamp,
|
|
1700
2243
|
params.accountId,
|
|
1701
2244
|
);
|
|
@@ -1748,86 +2291,26 @@ const BITRIX24_DISCOVERY_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'dele
|
|
|
1748
2291
|
function buildMessageToolButtonsSchema(): Record<string, unknown> {
|
|
1749
2292
|
return {
|
|
1750
2293
|
type: 'array',
|
|
1751
|
-
description: 'Optional Bitrix24 message buttons as rows of button objects.',
|
|
2294
|
+
description: 'Optional Bitrix24 message buttons as rows of button objects. Each button object can include text, optional callback_data and optional style.',
|
|
1752
2295
|
items: {
|
|
1753
2296
|
type: 'array',
|
|
1754
2297
|
items: {
|
|
1755
2298
|
type: 'object',
|
|
1756
|
-
|
|
1757
|
-
properties: {
|
|
1758
|
-
text: {
|
|
1759
|
-
type: 'string',
|
|
1760
|
-
description: 'Visible button label.',
|
|
1761
|
-
},
|
|
1762
|
-
callback_data: {
|
|
1763
|
-
type: 'string',
|
|
1764
|
-
description: 'Registered command like /help, or any plain text payload to send when tapped.',
|
|
1765
|
-
},
|
|
1766
|
-
style: {
|
|
1767
|
-
type: 'string',
|
|
1768
|
-
enum: ['primary', 'attention', 'danger'],
|
|
1769
|
-
description: 'Optional Bitrix24 button accent.',
|
|
1770
|
-
},
|
|
1771
|
-
},
|
|
1772
|
-
required: ['text'],
|
|
2299
|
+
description: 'Single button object. Use text, optional callback_data, and optional style=primary|attention|danger.',
|
|
1773
2300
|
},
|
|
1774
2301
|
},
|
|
1775
2302
|
};
|
|
1776
2303
|
}
|
|
1777
2304
|
|
|
1778
2305
|
function buildMessageToolAttachSchema(): Record<string, unknown> {
|
|
1779
|
-
const blockSchema = {
|
|
1780
|
-
type: 'object',
|
|
1781
|
-
description: 'Single Bitrix24 ATTACH rich block. Supported top-level keys are MESSAGE, LINK, IMAGE, FILE, DELIMITER, GRID or USER.',
|
|
1782
|
-
};
|
|
1783
|
-
|
|
1784
2306
|
return {
|
|
1785
|
-
description: 'Bitrix24 ATTACH rich layout blocks. Use this for rich cards, not for binary file uploads.
|
|
1786
|
-
anyOf: [
|
|
1787
|
-
{
|
|
1788
|
-
type: 'object',
|
|
1789
|
-
additionalProperties: false,
|
|
1790
|
-
properties: {
|
|
1791
|
-
ID: { type: 'integer' },
|
|
1792
|
-
COLOR_TOKEN: {
|
|
1793
|
-
type: 'string',
|
|
1794
|
-
enum: ['primary', 'secondary', 'alert', 'base'],
|
|
1795
|
-
},
|
|
1796
|
-
COLOR: { type: 'string' },
|
|
1797
|
-
BLOCKS: {
|
|
1798
|
-
type: 'array',
|
|
1799
|
-
items: blockSchema,
|
|
1800
|
-
},
|
|
1801
|
-
},
|
|
1802
|
-
required: ['BLOCKS'],
|
|
1803
|
-
},
|
|
1804
|
-
{
|
|
1805
|
-
type: 'array',
|
|
1806
|
-
items: blockSchema,
|
|
1807
|
-
},
|
|
1808
|
-
],
|
|
2307
|
+
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.',
|
|
1809
2308
|
};
|
|
1810
2309
|
}
|
|
1811
2310
|
|
|
1812
2311
|
function buildMessageToolIdListSchema(description: string): Record<string, unknown> {
|
|
1813
2312
|
return {
|
|
1814
|
-
|
|
1815
|
-
{
|
|
1816
|
-
type: 'integer',
|
|
1817
|
-
description,
|
|
1818
|
-
},
|
|
1819
|
-
{
|
|
1820
|
-
type: 'string',
|
|
1821
|
-
description: `${description} Can also be a comma-separated list or JSON array string.`,
|
|
1822
|
-
},
|
|
1823
|
-
{
|
|
1824
|
-
type: 'array',
|
|
1825
|
-
description,
|
|
1826
|
-
items: {
|
|
1827
|
-
type: 'integer',
|
|
1828
|
-
},
|
|
1829
|
-
},
|
|
1830
|
-
],
|
|
2313
|
+
description: `${description} Can be an integer, a string, or an array of integers.`,
|
|
1831
2314
|
};
|
|
1832
2315
|
}
|
|
1833
2316
|
|
|
@@ -1866,93 +2349,507 @@ function describeBitrix24MessageTool(params: {
|
|
|
1866
2349
|
return {
|
|
1867
2350
|
actions: [...BITRIX24_DISCOVERY_ACTION_NAMES],
|
|
1868
2351
|
capabilities: ['interactive', 'buttons', 'cards'],
|
|
1869
|
-
schema:
|
|
1870
|
-
{
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2352
|
+
schema: {
|
|
2353
|
+
properties: {
|
|
2354
|
+
buttons: buildMessageToolButtonsSchema(),
|
|
2355
|
+
attach: buildMessageToolAttachSchema(),
|
|
2356
|
+
forwardMessageIds: buildMessageToolIdListSchema(
|
|
2357
|
+
'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.',
|
|
2358
|
+
),
|
|
2359
|
+
forwardIds: buildMessageToolIdListSchema(
|
|
2360
|
+
'Alias for forwardMessageIds. Use with action="send".',
|
|
2361
|
+
),
|
|
2362
|
+
replyToId: buildMessageToolIdListSchema(
|
|
2363
|
+
'Bitrix24 message id to reply to natively inside the current dialog.',
|
|
2364
|
+
),
|
|
2365
|
+
replyToMessageId: buildMessageToolIdListSchema(
|
|
2366
|
+
'Alias for replyToId.',
|
|
2367
|
+
),
|
|
1874
2368
|
},
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2369
|
+
},
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function extractBitrix24ToolSend(args: Record<string, unknown>): ExtractedToolSendTarget | null {
|
|
2374
|
+
if ((typeof args.action === 'string' ? args.action.trim() : '') !== 'sendMessage') {
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
const to = typeof args.to === 'string' ? normalizeBitrix24DialogTarget(args.to) : '';
|
|
2379
|
+
if (!to) {
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
const accountId = typeof args.accountId === 'string'
|
|
2384
|
+
? args.accountId.trim()
|
|
2385
|
+
: undefined;
|
|
2386
|
+
const threadId = typeof args.threadId === 'number'
|
|
2387
|
+
? String(args.threadId)
|
|
2388
|
+
: typeof args.threadId === 'string'
|
|
2389
|
+
? args.threadId.trim()
|
|
2390
|
+
: '';
|
|
2391
|
+
|
|
2392
|
+
return {
|
|
2393
|
+
to,
|
|
2394
|
+
...(accountId ? { accountId } : {}),
|
|
2395
|
+
...(threadId ? { threadId } : {}),
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function readActionTextParam(params: Record<string, unknown>): string | null {
|
|
2400
|
+
const rawText = params.message ?? params.text ?? params.content;
|
|
2401
|
+
return typeof rawText === 'string' ? rawText : null;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
2405
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function isAttachColorToken(value: unknown): value is B24AttachColorToken {
|
|
2409
|
+
return value === 'primary'
|
|
2410
|
+
|| value === 'secondary'
|
|
2411
|
+
|| value === 'alert'
|
|
2412
|
+
|| value === 'base';
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
function isPresent<T>(value: T | undefined | null): value is T {
|
|
2416
|
+
return value != null;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function isAttachBlock(value: unknown): value is B24AttachBlock {
|
|
2420
|
+
return isPlainObject(value) && Object.keys(value).length > 0;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function isBitrix24Attach(value: unknown): value is B24Attach {
|
|
2424
|
+
if (Array.isArray(value)) {
|
|
2425
|
+
return value.length > 0 && value.every(isAttachBlock);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
if (!isPlainObject(value) || !Array.isArray(value.BLOCKS) || value.BLOCKS.length === 0) {
|
|
2429
|
+
return false;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
if (value.COLOR_TOKEN !== undefined && !isAttachColorToken(value.COLOR_TOKEN)) {
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
return value.BLOCKS.every(isAttachBlock);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function readAttachString(value: unknown): string | undefined {
|
|
2440
|
+
if (typeof value !== 'string') {
|
|
2441
|
+
return undefined;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const trimmed = value.trim();
|
|
2445
|
+
return trimmed ? trimmed : undefined;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function readAttachNumber(value: unknown): number | undefined {
|
|
2449
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
2450
|
+
return Math.trunc(value);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
if (typeof value === 'string') {
|
|
2454
|
+
const trimmed = value.trim();
|
|
2455
|
+
if (!trimmed) {
|
|
2456
|
+
return undefined;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
const normalized = Number(trimmed);
|
|
2460
|
+
if (Number.isFinite(normalized)) {
|
|
2461
|
+
return Math.trunc(normalized);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
return undefined;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function normalizeAttachColorTokenValue(value: unknown): B24AttachColorToken | undefined {
|
|
2469
|
+
if (value === undefined) {
|
|
2470
|
+
return undefined;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
if (typeof value !== 'string') {
|
|
2474
|
+
return undefined;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
const normalized = value.trim().toLowerCase();
|
|
2478
|
+
return isAttachColorToken(normalized) ? normalized : undefined;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function normalizeAttachGridDisplayValue(value: unknown): B24AttachGridDisplay | undefined {
|
|
2482
|
+
if (typeof value !== 'string') {
|
|
2483
|
+
return undefined;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const normalized = value.trim().toUpperCase();
|
|
2487
|
+
return normalized === 'BLOCK'
|
|
2488
|
+
|| normalized === 'LINE'
|
|
2489
|
+
|| normalized === 'ROW'
|
|
2490
|
+
|| normalized === 'TABLE'
|
|
2491
|
+
? normalized
|
|
2492
|
+
: undefined;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function normalizeAttachGridItem(rawValue: unknown): B24AttachGridItem | undefined {
|
|
2496
|
+
if (!isPlainObject(rawValue)) {
|
|
2497
|
+
return undefined;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
const display = normalizeAttachGridDisplayValue(rawValue.DISPLAY) ?? 'LINE';
|
|
2501
|
+
const name = readAttachString(rawValue.NAME);
|
|
2502
|
+
const value = readAttachString(rawValue.VALUE);
|
|
2503
|
+
const width = readAttachNumber(rawValue.WIDTH);
|
|
2504
|
+
const height = readAttachNumber(rawValue.HEIGHT);
|
|
2505
|
+
const colorToken = normalizeAttachColorTokenValue(rawValue.COLOR_TOKEN);
|
|
2506
|
+
const color = readAttachString(rawValue.COLOR);
|
|
2507
|
+
const link = readAttachString(rawValue.LINK);
|
|
2508
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
2509
|
+
const chatId = readAttachNumber(rawValue.CHAT_ID);
|
|
2510
|
+
|
|
2511
|
+
if (!name && !value && !link && userId === undefined && chatId === undefined) {
|
|
2512
|
+
return undefined;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
return {
|
|
2516
|
+
DISPLAY: display,
|
|
2517
|
+
...(name ? { NAME: name } : {}),
|
|
2518
|
+
...(value ? { VALUE: value } : {}),
|
|
2519
|
+
...(width !== undefined ? { WIDTH: width } : {}),
|
|
2520
|
+
...(height !== undefined ? { HEIGHT: height } : {}),
|
|
2521
|
+
...(colorToken ? { COLOR_TOKEN: colorToken } : {}),
|
|
2522
|
+
...(color ? { COLOR: color } : {}),
|
|
2523
|
+
...(link ? { LINK: link } : {}),
|
|
2524
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
2525
|
+
...(chatId !== undefined ? { CHAT_ID: chatId } : {}),
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function normalizeAttachLinkValue(rawValue: unknown, aliasSource?: Record<string, unknown>): B24AttachLinkValue | undefined {
|
|
2530
|
+
if (typeof rawValue === 'string') {
|
|
2531
|
+
const link = readAttachString(rawValue);
|
|
2532
|
+
if (!link) {
|
|
2533
|
+
return undefined;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
const aliasName = aliasSource
|
|
2537
|
+
? readAttachString(aliasSource.NAME) ?? readAttachString(aliasSource.TITLE)
|
|
2538
|
+
: undefined;
|
|
2539
|
+
const aliasDesc = aliasSource ? readAttachString(aliasSource.DESC) : undefined;
|
|
2540
|
+
const aliasHtml = aliasSource ? readAttachString(aliasSource.HTML) : undefined;
|
|
2541
|
+
const aliasPreview = aliasSource ? readAttachString(aliasSource.PREVIEW) : undefined;
|
|
2542
|
+
const aliasWidth = aliasSource ? readAttachNumber(aliasSource.WIDTH) : undefined;
|
|
2543
|
+
const aliasHeight = aliasSource ? readAttachNumber(aliasSource.HEIGHT) : undefined;
|
|
2544
|
+
const aliasUserId = aliasSource ? readAttachNumber(aliasSource.USER_ID) : undefined;
|
|
2545
|
+
const aliasChatId = aliasSource ? readAttachNumber(aliasSource.CHAT_ID) : undefined;
|
|
2546
|
+
const aliasNetworkId = aliasSource ? readAttachString(aliasSource.NETWORK_ID) : undefined;
|
|
2547
|
+
|
|
2548
|
+
return {
|
|
2549
|
+
LINK: link,
|
|
2550
|
+
...(aliasName ? { NAME: aliasName } : {}),
|
|
2551
|
+
...(aliasDesc ? { DESC: aliasDesc } : {}),
|
|
2552
|
+
...(aliasHtml ? { HTML: aliasHtml } : {}),
|
|
2553
|
+
...(aliasPreview ? { PREVIEW: aliasPreview } : {}),
|
|
2554
|
+
...(aliasWidth !== undefined ? { WIDTH: aliasWidth } : {}),
|
|
2555
|
+
...(aliasHeight !== undefined ? { HEIGHT: aliasHeight } : {}),
|
|
2556
|
+
...(aliasUserId !== undefined ? { USER_ID: aliasUserId } : {}),
|
|
2557
|
+
...(aliasChatId !== undefined ? { CHAT_ID: aliasChatId } : {}),
|
|
2558
|
+
...(aliasNetworkId ? { NETWORK_ID: aliasNetworkId } : {}),
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
if (!isPlainObject(rawValue)) {
|
|
2563
|
+
return undefined;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
const link = readAttachString(rawValue.LINK);
|
|
2567
|
+
if (!link) {
|
|
2568
|
+
return undefined;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
const name = readAttachString(rawValue.NAME) ?? readAttachString(rawValue.TITLE);
|
|
2572
|
+
const desc = readAttachString(rawValue.DESC);
|
|
2573
|
+
const html = readAttachString(rawValue.HTML);
|
|
2574
|
+
const preview = readAttachString(rawValue.PREVIEW);
|
|
2575
|
+
const width = readAttachNumber(rawValue.WIDTH);
|
|
2576
|
+
const height = readAttachNumber(rawValue.HEIGHT);
|
|
2577
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
2578
|
+
const chatId = readAttachNumber(rawValue.CHAT_ID);
|
|
2579
|
+
const networkId = readAttachString(rawValue.NETWORK_ID);
|
|
2580
|
+
|
|
2581
|
+
return {
|
|
2582
|
+
LINK: link,
|
|
2583
|
+
...(name ? { NAME: name } : {}),
|
|
2584
|
+
...(desc ? { DESC: desc } : {}),
|
|
2585
|
+
...(html ? { HTML: html } : {}),
|
|
2586
|
+
...(preview ? { PREVIEW: preview } : {}),
|
|
2587
|
+
...(width !== undefined ? { WIDTH: width } : {}),
|
|
2588
|
+
...(height !== undefined ? { HEIGHT: height } : {}),
|
|
2589
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
2590
|
+
...(chatId !== undefined ? { CHAT_ID: chatId } : {}),
|
|
2591
|
+
...(networkId ? { NETWORK_ID: networkId } : {}),
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
function normalizeAttachImageItem(rawValue: unknown): B24AttachImageItem | undefined {
|
|
2596
|
+
const normalized = normalizeAttachLinkValue(rawValue);
|
|
2597
|
+
if (!normalized) {
|
|
2598
|
+
return undefined;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
return {
|
|
2602
|
+
LINK: normalized.LINK,
|
|
2603
|
+
...(normalized.NAME ? { NAME: normalized.NAME } : {}),
|
|
2604
|
+
...(normalized.PREVIEW ? { PREVIEW: normalized.PREVIEW } : {}),
|
|
2605
|
+
...(normalized.WIDTH !== undefined ? { WIDTH: normalized.WIDTH } : {}),
|
|
2606
|
+
...(normalized.HEIGHT !== undefined ? { HEIGHT: normalized.HEIGHT } : {}),
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function normalizeAttachFileItem(rawValue: unknown): B24AttachFileItem | undefined {
|
|
2611
|
+
if (typeof rawValue === 'string') {
|
|
2612
|
+
const link = readAttachString(rawValue);
|
|
2613
|
+
return link ? { LINK: link } : undefined;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
if (!isPlainObject(rawValue)) {
|
|
2617
|
+
return undefined;
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
const link = readAttachString(rawValue.LINK);
|
|
2621
|
+
if (!link) {
|
|
2622
|
+
return undefined;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const name = readAttachString(rawValue.NAME);
|
|
2626
|
+
const size = readAttachNumber(rawValue.SIZE);
|
|
2627
|
+
|
|
2628
|
+
return {
|
|
2629
|
+
LINK: link,
|
|
2630
|
+
...(name ? { NAME: name } : {}),
|
|
2631
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
function normalizeAttachUserValue(rawValue: unknown, aliasSource?: Record<string, unknown>): B24AttachUserValue | undefined {
|
|
2636
|
+
if (typeof rawValue === 'number' || typeof rawValue === 'string') {
|
|
2637
|
+
const userId = readAttachNumber(rawValue);
|
|
2638
|
+
if (userId === undefined) {
|
|
2639
|
+
return undefined;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
const aliasName = aliasSource
|
|
2643
|
+
? readAttachString(aliasSource.NAME) ?? readAttachString(aliasSource.USER_NAME)
|
|
2644
|
+
: undefined;
|
|
2645
|
+
const aliasAvatar = aliasSource ? readAttachString(aliasSource.AVATAR) : undefined;
|
|
2646
|
+
const aliasLink = aliasSource ? readAttachString(aliasSource.LINK) : undefined;
|
|
2647
|
+
const aliasNetworkId = aliasSource ? readAttachString(aliasSource.NETWORK_ID) : undefined;
|
|
2648
|
+
|
|
2649
|
+
return {
|
|
2650
|
+
USER_ID: userId,
|
|
2651
|
+
...(aliasName ? { NAME: aliasName } : {}),
|
|
2652
|
+
...(aliasAvatar ? { AVATAR: aliasAvatar } : {}),
|
|
2653
|
+
...(aliasLink ? { LINK: aliasLink } : {}),
|
|
2654
|
+
...(aliasNetworkId ? { NETWORK_ID: aliasNetworkId } : {}),
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
if (!isPlainObject(rawValue)) {
|
|
2659
|
+
return undefined;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
2663
|
+
const name = readAttachString(rawValue.NAME) ?? readAttachString(rawValue.USER_NAME);
|
|
2664
|
+
const avatar = readAttachString(rawValue.AVATAR);
|
|
2665
|
+
const link = readAttachString(rawValue.LINK);
|
|
2666
|
+
const networkId = readAttachString(rawValue.NETWORK_ID);
|
|
2667
|
+
|
|
2668
|
+
if (userId === undefined && !name && !avatar && !link && !networkId) {
|
|
2669
|
+
return undefined;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
return {
|
|
2673
|
+
...(name ? { NAME: name } : {}),
|
|
2674
|
+
...(avatar ? { AVATAR: avatar } : {}),
|
|
2675
|
+
...(link ? { LINK: link } : {}),
|
|
2676
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
2677
|
+
...(networkId ? { NETWORK_ID: networkId } : {}),
|
|
1893
2678
|
};
|
|
1894
2679
|
}
|
|
1895
2680
|
|
|
1896
|
-
function
|
|
1897
|
-
if ((
|
|
1898
|
-
return
|
|
2681
|
+
function normalizeAttachBlock(rawValue: unknown): B24AttachBlock | undefined {
|
|
2682
|
+
if (!isPlainObject(rawValue)) {
|
|
2683
|
+
return undefined;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
const message = readAttachString(rawValue.MESSAGE);
|
|
2687
|
+
if (message && Object.keys(rawValue).every((key) => key === 'MESSAGE' || key === 'TYPE')) {
|
|
2688
|
+
return { MESSAGE: message };
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
if ('LINK' in rawValue && rawValue.LINK !== undefined) {
|
|
2692
|
+
const linkValue = normalizeAttachLinkValue(rawValue.LINK, rawValue);
|
|
2693
|
+
if (linkValue) {
|
|
2694
|
+
return { LINK: linkValue };
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
if ('IMAGE' in rawValue && rawValue.IMAGE !== undefined) {
|
|
2699
|
+
const rawImage = rawValue.IMAGE;
|
|
2700
|
+
const imageItems = Array.isArray(rawImage)
|
|
2701
|
+
? rawImage.map(normalizeAttachImageItem).filter(isPresent)
|
|
2702
|
+
: [normalizeAttachImageItem(rawImage)].filter(isPresent);
|
|
2703
|
+
if (imageItems.length > 0) {
|
|
2704
|
+
return { IMAGE: imageItems.length === 1 ? imageItems[0] : imageItems };
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
if ('FILE' in rawValue && rawValue.FILE !== undefined) {
|
|
2709
|
+
const rawFile = rawValue.FILE;
|
|
2710
|
+
const fileItems = Array.isArray(rawFile)
|
|
2711
|
+
? rawFile.map(normalizeAttachFileItem).filter(isPresent)
|
|
2712
|
+
: [normalizeAttachFileItem(rawFile)].filter(isPresent);
|
|
2713
|
+
if (fileItems.length > 0) {
|
|
2714
|
+
return { FILE: fileItems.length === 1 ? fileItems[0] : fileItems };
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
if ('DELIMITER' in rawValue && isPlainObject(rawValue.DELIMITER)) {
|
|
2719
|
+
const delimiter = rawValue.DELIMITER as Record<string, unknown>;
|
|
2720
|
+
const size = readAttachNumber(delimiter.SIZE);
|
|
2721
|
+
const color = readAttachString(delimiter.COLOR);
|
|
2722
|
+
if (size !== undefined || color) {
|
|
2723
|
+
return {
|
|
2724
|
+
DELIMITER: {
|
|
2725
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
2726
|
+
...(color ? { COLOR: color } : {}),
|
|
2727
|
+
},
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
if ('GRID' in rawValue && Array.isArray(rawValue.GRID)) {
|
|
2733
|
+
const gridItems = rawValue.GRID.map(normalizeAttachGridItem).filter(isPresent);
|
|
2734
|
+
if (gridItems.length > 0) {
|
|
2735
|
+
return { GRID: gridItems };
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
if ('USER' in rawValue && rawValue.USER !== undefined) {
|
|
2740
|
+
const userValue = normalizeAttachUserValue(rawValue.USER, rawValue);
|
|
2741
|
+
if (userValue) {
|
|
2742
|
+
return { USER: userValue };
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
const rawType = readAttachString(rawValue.TYPE)?.toUpperCase();
|
|
2747
|
+
if (!rawType) {
|
|
2748
|
+
return undefined;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
if (rawType === 'MESSAGE') {
|
|
2752
|
+
const title = readAttachString(rawValue.TITLE);
|
|
2753
|
+
return message
|
|
2754
|
+
? { MESSAGE: message }
|
|
2755
|
+
: title
|
|
2756
|
+
? { MESSAGE: `[B]${title}[/B]` }
|
|
2757
|
+
: undefined;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
if (rawType === 'TITLE') {
|
|
2761
|
+
const title = readAttachString(rawValue.TITLE) ?? message;
|
|
2762
|
+
return title ? { MESSAGE: `[B]${title}[/B]` } : undefined;
|
|
1899
2763
|
}
|
|
1900
2764
|
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
return
|
|
2765
|
+
if (rawType === 'LINK') {
|
|
2766
|
+
const linkValue = normalizeAttachLinkValue(rawValue.LINK ?? rawValue, rawValue);
|
|
2767
|
+
return linkValue ? { LINK: linkValue } : undefined;
|
|
1904
2768
|
}
|
|
1905
2769
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
: undefined;
|
|
1909
|
-
|
|
1910
|
-
? String(args.threadId)
|
|
1911
|
-
: typeof args.threadId === 'string'
|
|
1912
|
-
? args.threadId.trim()
|
|
1913
|
-
: '';
|
|
2770
|
+
if (rawType === 'USER') {
|
|
2771
|
+
const userValue = normalizeAttachUserValue(rawValue.USER ?? rawValue.USER_ID ?? rawValue, rawValue);
|
|
2772
|
+
return userValue ? { USER: userValue } : undefined;
|
|
2773
|
+
}
|
|
1914
2774
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2775
|
+
if (rawType === 'GRID') {
|
|
2776
|
+
const rawGridItems = Array.isArray(rawValue.GRID)
|
|
2777
|
+
? rawValue.GRID
|
|
2778
|
+
: Array.isArray(rawValue.ITEMS)
|
|
2779
|
+
? rawValue.ITEMS
|
|
2780
|
+
: Array.isArray(rawValue.ROWS)
|
|
2781
|
+
? rawValue.ROWS
|
|
2782
|
+
: [];
|
|
2783
|
+
const gridItems = rawGridItems.map(normalizeAttachGridItem).filter(isPresent);
|
|
2784
|
+
return gridItems.length > 0 ? { GRID: gridItems } : undefined;
|
|
2785
|
+
}
|
|
1921
2786
|
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2787
|
+
if (rawType === 'DELIMITER') {
|
|
2788
|
+
const size = readAttachNumber(rawValue.SIZE);
|
|
2789
|
+
const color = readAttachString(rawValue.COLOR);
|
|
2790
|
+
return size !== undefined || color
|
|
2791
|
+
? {
|
|
2792
|
+
DELIMITER: {
|
|
2793
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
2794
|
+
...(color ? { COLOR: color } : {}),
|
|
2795
|
+
},
|
|
2796
|
+
}
|
|
2797
|
+
: undefined;
|
|
2798
|
+
}
|
|
1926
2799
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2800
|
+
if (rawType === 'IMAGE') {
|
|
2801
|
+
const rawImage = rawValue.IMAGE ?? rawValue.LINK ?? rawValue;
|
|
2802
|
+
const imageItems = Array.isArray(rawImage)
|
|
2803
|
+
? rawImage.map(normalizeAttachImageItem).filter(isPresent)
|
|
2804
|
+
: [normalizeAttachImageItem(rawImage)].filter(isPresent);
|
|
2805
|
+
return imageItems.length > 0
|
|
2806
|
+
? { IMAGE: imageItems.length === 1 ? imageItems[0] : imageItems }
|
|
2807
|
+
: undefined;
|
|
2808
|
+
}
|
|
1930
2809
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2810
|
+
if (rawType === 'FILE') {
|
|
2811
|
+
const rawFile = rawValue.FILE ?? rawValue.LINK ?? rawValue;
|
|
2812
|
+
const fileItems = Array.isArray(rawFile)
|
|
2813
|
+
? rawFile.map(normalizeAttachFileItem).filter(isPresent)
|
|
2814
|
+
: [normalizeAttachFileItem(rawFile)].filter(isPresent);
|
|
2815
|
+
return fileItems.length > 0
|
|
2816
|
+
? { FILE: fileItems.length === 1 ? fileItems[0] : fileItems }
|
|
2817
|
+
: undefined;
|
|
2818
|
+
}
|
|
1937
2819
|
|
|
1938
|
-
|
|
1939
|
-
return isPlainObject(value) && Object.keys(value).length > 0;
|
|
2820
|
+
return undefined;
|
|
1940
2821
|
}
|
|
1941
2822
|
|
|
1942
|
-
function
|
|
1943
|
-
if (Array.isArray(
|
|
1944
|
-
|
|
2823
|
+
function normalizeBitrix24Attach(rawValue: unknown): B24Attach | undefined {
|
|
2824
|
+
if (Array.isArray(rawValue)) {
|
|
2825
|
+
const blocks = rawValue.map(normalizeAttachBlock).filter(isPresent);
|
|
2826
|
+
return blocks.length > 0 ? blocks : undefined;
|
|
1945
2827
|
}
|
|
1946
2828
|
|
|
1947
|
-
if (!isPlainObject(
|
|
1948
|
-
return
|
|
2829
|
+
if (!isPlainObject(rawValue)) {
|
|
2830
|
+
return undefined;
|
|
1949
2831
|
}
|
|
1950
2832
|
|
|
1951
|
-
if (
|
|
1952
|
-
|
|
2833
|
+
if (Array.isArray(rawValue.BLOCKS)) {
|
|
2834
|
+
const blocks = rawValue.BLOCKS.map(normalizeAttachBlock).filter(isPresent);
|
|
2835
|
+
if (blocks.length === 0) {
|
|
2836
|
+
return undefined;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
const id = readAttachNumber(rawValue.ID);
|
|
2840
|
+
const colorToken = normalizeAttachColorTokenValue(rawValue.COLOR_TOKEN);
|
|
2841
|
+
const color = readAttachString(rawValue.COLOR);
|
|
2842
|
+
|
|
2843
|
+
return {
|
|
2844
|
+
...(id !== undefined ? { ID: id } : {}),
|
|
2845
|
+
...(colorToken ? { COLOR_TOKEN: colorToken } : {}),
|
|
2846
|
+
...(color ? { COLOR: color } : {}),
|
|
2847
|
+
BLOCKS: blocks,
|
|
2848
|
+
};
|
|
1953
2849
|
}
|
|
1954
2850
|
|
|
1955
|
-
|
|
2851
|
+
const block = normalizeAttachBlock(rawValue);
|
|
2852
|
+
return block ? [block] : undefined;
|
|
1956
2853
|
}
|
|
1957
2854
|
|
|
1958
2855
|
function parseActionAttach(rawValue: unknown): B24Attach | undefined {
|
|
@@ -1974,7 +2871,7 @@ function parseActionAttach(rawValue: unknown): B24Attach | undefined {
|
|
|
1974
2871
|
}
|
|
1975
2872
|
}
|
|
1976
2873
|
|
|
1977
|
-
return
|
|
2874
|
+
return normalizeBitrix24Attach(parsed);
|
|
1978
2875
|
}
|
|
1979
2876
|
|
|
1980
2877
|
function hasMeaningfulAttachInput(rawValue: unknown): boolean {
|
|
@@ -2127,8 +3024,11 @@ function collectActionMediaUrls(params: Record<string, unknown>): string[] {
|
|
|
2127
3024
|
|
|
2128
3025
|
append(params.mediaUrl);
|
|
2129
3026
|
append(params.mediaUrls);
|
|
3027
|
+
append(params.media);
|
|
2130
3028
|
append(params.filePath);
|
|
2131
3029
|
append(params.filePaths);
|
|
3030
|
+
append(params.path);
|
|
3031
|
+
append(params.paths);
|
|
2132
3032
|
|
|
2133
3033
|
return mediaUrls;
|
|
2134
3034
|
}
|
|
@@ -2685,15 +3585,23 @@ async function uploadOutboundMedia(params: {
|
|
|
2685
3585
|
mediaService: MediaService;
|
|
2686
3586
|
mediaUrls: string[];
|
|
2687
3587
|
sendCtx: SendContext;
|
|
3588
|
+
currentMessageId?: string | number;
|
|
3589
|
+
accountId?: string;
|
|
2688
3590
|
initialMessage?: string;
|
|
2689
3591
|
}): Promise<string> {
|
|
2690
3592
|
let lastMessageId = '';
|
|
2691
3593
|
let message = params.initialMessage;
|
|
2692
3594
|
|
|
2693
3595
|
for (const mediaUrl of params.mediaUrls) {
|
|
3596
|
+
const outboundFileName = resolveSemanticOutboundFileName({
|
|
3597
|
+
requestedFileName: basename(mediaUrl),
|
|
3598
|
+
dialogId: params.sendCtx.dialogId,
|
|
3599
|
+
currentMessageId: params.currentMessageId,
|
|
3600
|
+
accountId: params.accountId,
|
|
3601
|
+
});
|
|
2694
3602
|
const result = await params.mediaService.uploadMediaToChat({
|
|
2695
3603
|
localPath: mediaUrl,
|
|
2696
|
-
fileName:
|
|
3604
|
+
fileName: outboundFileName,
|
|
2697
3605
|
webhookUrl: params.sendCtx.webhookUrl,
|
|
2698
3606
|
bot: params.sendCtx.bot,
|
|
2699
3607
|
dialogId: params.sendCtx.dialogId,
|
|
@@ -2701,7 +3609,7 @@ async function uploadOutboundMedia(params: {
|
|
|
2701
3609
|
});
|
|
2702
3610
|
|
|
2703
3611
|
if (!result.ok) {
|
|
2704
|
-
throw new Error(`Failed to upload media: ${
|
|
3612
|
+
throw new Error(`Failed to upload media: ${outboundFileName}`);
|
|
2705
3613
|
}
|
|
2706
3614
|
|
|
2707
3615
|
if (result.messageId) {
|
|
@@ -2834,6 +3742,8 @@ export const bitrix24Plugin = {
|
|
|
2834
3742
|
mediaService: gatewayState.mediaService,
|
|
2835
3743
|
sendCtx,
|
|
2836
3744
|
mediaUrls,
|
|
3745
|
+
currentMessageId: ctx.currentMessageId as string | number | undefined,
|
|
3746
|
+
accountId: ctx.accountId,
|
|
2837
3747
|
initialMessage: ctx.text,
|
|
2838
3748
|
});
|
|
2839
3749
|
return { messageId };
|
|
@@ -2877,6 +3787,8 @@ export const bitrix24Plugin = {
|
|
|
2877
3787
|
mediaService: gatewayState.mediaService,
|
|
2878
3788
|
sendCtx,
|
|
2879
3789
|
mediaUrls,
|
|
3790
|
+
currentMessageId: ctx.currentMessageId as string | number | undefined,
|
|
3791
|
+
accountId: ctx.accountId,
|
|
2880
3792
|
initialMessage,
|
|
2881
3793
|
});
|
|
2882
3794
|
|
|
@@ -2967,6 +3879,9 @@ export const bitrix24Plugin = {
|
|
|
2967
3879
|
content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }],
|
|
2968
3880
|
details: payload,
|
|
2969
3881
|
});
|
|
3882
|
+
const markSuccessfulAction = (currentMessageId: string | number | undefined): void => {
|
|
3883
|
+
markRecentSuccessfulAction(currentMessageId, ctx.accountId);
|
|
3884
|
+
};
|
|
2970
3885
|
|
|
2971
3886
|
// ─── Send with buttons ──────────────────────────────────────────────
|
|
2972
3887
|
if (ctx.action === 'send') {
|
|
@@ -2984,6 +3899,7 @@ export const bitrix24Plugin = {
|
|
|
2984
3899
|
}
|
|
2985
3900
|
|
|
2986
3901
|
const sendCtx: SendContext = { webhookUrl: config.webhookUrl, bot, dialogId: to };
|
|
3902
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ dialogId: to });
|
|
2987
3903
|
const messageText = readActionTextParam(ctx.params);
|
|
2988
3904
|
const message = typeof messageText === 'string' ? messageText.trim() : '';
|
|
2989
3905
|
const keyboard = parseActionKeyboard(rawButtons);
|
|
@@ -3026,6 +3942,26 @@ export const bitrix24Plugin = {
|
|
|
3026
3942
|
});
|
|
3027
3943
|
}
|
|
3028
3944
|
|
|
3945
|
+
const duplicateMediaDelivery = mediaUrls.length > 0
|
|
3946
|
+
? findRecentMediaDelivery({
|
|
3947
|
+
dialogId: to,
|
|
3948
|
+
currentMessageId: currentToolMessageId,
|
|
3949
|
+
mediaUrls,
|
|
3950
|
+
accountId: ctx.accountId,
|
|
3951
|
+
})
|
|
3952
|
+
: null;
|
|
3953
|
+
if (duplicateMediaDelivery) {
|
|
3954
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3955
|
+
return toolResult({
|
|
3956
|
+
channel: 'bitrix24',
|
|
3957
|
+
to,
|
|
3958
|
+
via: 'direct',
|
|
3959
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
3960
|
+
duplicateSuppressed: true,
|
|
3961
|
+
result: { messageId: duplicateMediaDelivery.messageId ?? '' },
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3029
3965
|
try {
|
|
3030
3966
|
if (explicitForwardMessages.length > 0) {
|
|
3031
3967
|
const normalizedForwardText = normalizeForwardActionText(messageText);
|
|
@@ -3042,7 +3978,16 @@ export const bitrix24Plugin = {
|
|
|
3042
3978
|
forwardMessages: explicitForwardMessages,
|
|
3043
3979
|
},
|
|
3044
3980
|
);
|
|
3981
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
3982
|
+
api: gatewayState.api,
|
|
3983
|
+
sendService: gatewayState.sendService,
|
|
3984
|
+
webhookUrl: config.webhookUrl,
|
|
3985
|
+
bot,
|
|
3986
|
+
dialogId: forwardAckDialogId,
|
|
3987
|
+
currentMessageId: currentToolMessageId,
|
|
3988
|
+
});
|
|
3045
3989
|
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
3990
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3046
3991
|
return toolResult({
|
|
3047
3992
|
channel: 'bitrix24',
|
|
3048
3993
|
to,
|
|
@@ -3062,7 +4007,16 @@ export const bitrix24Plugin = {
|
|
|
3062
4007
|
forwardMessages: explicitForwardMessages,
|
|
3063
4008
|
},
|
|
3064
4009
|
);
|
|
4010
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
4011
|
+
api: gatewayState.api,
|
|
4012
|
+
sendService: gatewayState.sendService,
|
|
4013
|
+
webhookUrl: config.webhookUrl,
|
|
4014
|
+
bot,
|
|
4015
|
+
dialogId: forwardAckDialogId,
|
|
4016
|
+
currentMessageId: currentToolMessageId,
|
|
4017
|
+
});
|
|
3065
4018
|
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
4019
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3066
4020
|
return toolResult({
|
|
3067
4021
|
channel: 'bitrix24',
|
|
3068
4022
|
to,
|
|
@@ -3074,7 +4028,7 @@ export const bitrix24Plugin = {
|
|
|
3074
4028
|
});
|
|
3075
4029
|
}
|
|
3076
4030
|
|
|
3077
|
-
if (referencedReplyMessageId && !attach && mediaUrls.length === 0) {
|
|
4031
|
+
if (canUseNativeReply && referencedReplyMessageId && !attach && mediaUrls.length === 0) {
|
|
3078
4032
|
let referencedMessageText = '';
|
|
3079
4033
|
|
|
3080
4034
|
try {
|
|
@@ -3092,13 +4046,22 @@ export const bitrix24Plugin = {
|
|
|
3092
4046
|
const normalizedMessage = normalizeComparableMessageText(message);
|
|
3093
4047
|
|
|
3094
4048
|
if (normalizedReferencedMessage && normalizedReferencedMessage === normalizedMessage) {
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
4049
|
+
const result = await gatewayState.sendService.sendText(
|
|
4050
|
+
sendCtx,
|
|
4051
|
+
'',
|
|
4052
|
+
{ forwardMessages: [referencedReplyMessageId] },
|
|
4053
|
+
);
|
|
4054
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
4055
|
+
api: gatewayState.api,
|
|
4056
|
+
sendService: gatewayState.sendService,
|
|
4057
|
+
webhookUrl: config.webhookUrl,
|
|
4058
|
+
bot,
|
|
4059
|
+
dialogId: forwardAckDialogId,
|
|
4060
|
+
currentMessageId: currentToolMessageId,
|
|
4061
|
+
});
|
|
4062
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
4063
|
+
markSuccessfulAction(currentToolMessageId);
|
|
4064
|
+
return toolResult({
|
|
3102
4065
|
channel: 'bitrix24',
|
|
3103
4066
|
to,
|
|
3104
4067
|
via: 'direct',
|
|
@@ -3109,21 +4072,25 @@ export const bitrix24Plugin = {
|
|
|
3109
4072
|
});
|
|
3110
4073
|
}
|
|
3111
4074
|
|
|
4075
|
+
const sendOptions = {
|
|
4076
|
+
...(keyboard ? { keyboard } : {}),
|
|
4077
|
+
...(canUseNativeReply ? { replyToMessageId: referencedReplyMessageId } : {}),
|
|
4078
|
+
};
|
|
3112
4079
|
const result = await gatewayState.sendService.sendText(
|
|
3113
4080
|
sendCtx,
|
|
3114
4081
|
message || ' ',
|
|
3115
|
-
|
|
3116
|
-
...(keyboard ? { keyboard } : {}),
|
|
3117
|
-
replyToMessageId: referencedReplyMessageId,
|
|
3118
|
-
},
|
|
4082
|
+
Object.keys(sendOptions).length > 0 ? sendOptions : undefined,
|
|
3119
4083
|
);
|
|
4084
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3120
4085
|
return toolResult({
|
|
3121
4086
|
channel: 'bitrix24',
|
|
3122
4087
|
to,
|
|
3123
4088
|
via: 'direct',
|
|
3124
4089
|
mediaUrl: null,
|
|
3125
|
-
|
|
3126
|
-
|
|
4090
|
+
...(canUseNativeReply ? {
|
|
4091
|
+
replied: true,
|
|
4092
|
+
replyToMessageId: referencedReplyMessageId,
|
|
4093
|
+
} : {}),
|
|
3127
4094
|
result: { messageId: String(result.messageId ?? '') },
|
|
3128
4095
|
});
|
|
3129
4096
|
}
|
|
@@ -3149,7 +4116,16 @@ export const bitrix24Plugin = {
|
|
|
3149
4116
|
'',
|
|
3150
4117
|
{ forwardMessages: [Number(previousInboundMessage.messageId)] },
|
|
3151
4118
|
);
|
|
4119
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
4120
|
+
api: gatewayState.api,
|
|
4121
|
+
sendService: gatewayState.sendService,
|
|
4122
|
+
webhookUrl: config.webhookUrl,
|
|
4123
|
+
bot,
|
|
4124
|
+
dialogId: forwardAckDialogId,
|
|
4125
|
+
currentMessageId: currentToolMessageId,
|
|
4126
|
+
});
|
|
3152
4127
|
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
4128
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3153
4129
|
return toolResult({
|
|
3154
4130
|
channel: 'bitrix24',
|
|
3155
4131
|
to,
|
|
@@ -3167,8 +4143,17 @@ export const bitrix24Plugin = {
|
|
|
3167
4143
|
mediaService: gatewayState.mediaService,
|
|
3168
4144
|
sendCtx,
|
|
3169
4145
|
mediaUrls,
|
|
4146
|
+
currentMessageId: currentToolMessageId,
|
|
4147
|
+
accountId: ctx.accountId,
|
|
3170
4148
|
initialMessage,
|
|
3171
4149
|
});
|
|
4150
|
+
markRecentMediaDelivery({
|
|
4151
|
+
dialogId: to,
|
|
4152
|
+
currentMessageId: currentToolMessageId,
|
|
4153
|
+
mediaUrls,
|
|
4154
|
+
accountId: ctx.accountId,
|
|
4155
|
+
messageId: uploadedMessageId,
|
|
4156
|
+
});
|
|
3172
4157
|
|
|
3173
4158
|
if ((message && !initialMessage) || keyboard || attach) {
|
|
3174
4159
|
if (attach) {
|
|
@@ -3182,6 +4167,7 @@ export const bitrix24Plugin = {
|
|
|
3182
4167
|
attach,
|
|
3183
4168
|
},
|
|
3184
4169
|
);
|
|
4170
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3185
4171
|
return toolResult({
|
|
3186
4172
|
channel: 'bitrix24',
|
|
3187
4173
|
to,
|
|
@@ -3196,6 +4182,7 @@ export const bitrix24Plugin = {
|
|
|
3196
4182
|
message || '',
|
|
3197
4183
|
keyboard ? { keyboard } : undefined,
|
|
3198
4184
|
);
|
|
4185
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3199
4186
|
return toolResult({
|
|
3200
4187
|
channel: 'bitrix24',
|
|
3201
4188
|
to,
|
|
@@ -3205,6 +4192,7 @@ export const bitrix24Plugin = {
|
|
|
3205
4192
|
});
|
|
3206
4193
|
}
|
|
3207
4194
|
|
|
4195
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3208
4196
|
return toolResult({
|
|
3209
4197
|
channel: 'bitrix24',
|
|
3210
4198
|
to,
|
|
@@ -3225,6 +4213,7 @@ export const bitrix24Plugin = {
|
|
|
3225
4213
|
attach,
|
|
3226
4214
|
},
|
|
3227
4215
|
);
|
|
4216
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3228
4217
|
return toolResult({
|
|
3229
4218
|
channel: 'bitrix24',
|
|
3230
4219
|
to,
|
|
@@ -3237,6 +4226,7 @@ export const bitrix24Plugin = {
|
|
|
3237
4226
|
const result = await gatewayState.sendService.sendText(
|
|
3238
4227
|
sendCtx, message || ' ', keyboard ? { keyboard } : undefined,
|
|
3239
4228
|
);
|
|
4229
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3240
4230
|
return toolResult({
|
|
3241
4231
|
channel: 'bitrix24',
|
|
3242
4232
|
to,
|
|
@@ -3246,6 +4236,14 @@ export const bitrix24Plugin = {
|
|
|
3246
4236
|
});
|
|
3247
4237
|
} catch (err) {
|
|
3248
4238
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4239
|
+
if (errMsg.startsWith('Failed to upload media: ')) {
|
|
4240
|
+
return toolResult({
|
|
4241
|
+
ok: false,
|
|
4242
|
+
reason: 'media_upload_failed',
|
|
4243
|
+
error: errMsg,
|
|
4244
|
+
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.',
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
3249
4247
|
return toolResult({ ok: false, error: errMsg });
|
|
3250
4248
|
}
|
|
3251
4249
|
}
|
|
@@ -3265,6 +4263,7 @@ export const bitrix24Plugin = {
|
|
|
3265
4263
|
|
|
3266
4264
|
const toolContext = (ctx as Record<string, unknown>).toolContext as
|
|
3267
4265
|
| { currentMessageId?: string | number } | undefined;
|
|
4266
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ dialogId: to });
|
|
3268
4267
|
const replyToMessageId = parseActionMessageIds(
|
|
3269
4268
|
params.replyToMessageId
|
|
3270
4269
|
?? params.replyToId
|
|
@@ -3300,31 +4299,38 @@ export const bitrix24Plugin = {
|
|
|
3300
4299
|
{
|
|
3301
4300
|
...(keyboard ? { keyboard } : {}),
|
|
3302
4301
|
attach,
|
|
3303
|
-
replyToMessageId,
|
|
4302
|
+
...(canUseNativeReply ? { replyToMessageId } : {}),
|
|
3304
4303
|
},
|
|
3305
4304
|
);
|
|
4305
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3306
4306
|
return toolResult({
|
|
3307
4307
|
ok: true,
|
|
3308
|
-
replied: true,
|
|
3309
4308
|
to,
|
|
3310
|
-
|
|
4309
|
+
...(canUseNativeReply ? {
|
|
4310
|
+
replied: true,
|
|
4311
|
+
replyToMessageId,
|
|
4312
|
+
} : {}),
|
|
3311
4313
|
messageId: String(messageId ?? ''),
|
|
3312
4314
|
});
|
|
3313
4315
|
}
|
|
3314
4316
|
|
|
4317
|
+
const sendOptions = {
|
|
4318
|
+
...(keyboard ? { keyboard } : {}),
|
|
4319
|
+
...(canUseNativeReply ? { replyToMessageId } : {}),
|
|
4320
|
+
};
|
|
3315
4321
|
const result = await gatewayState.sendService.sendText(
|
|
3316
4322
|
{ webhookUrl: config.webhookUrl, bot, dialogId: to },
|
|
3317
4323
|
normalizedText || ' ',
|
|
3318
|
-
|
|
3319
|
-
...(keyboard ? { keyboard } : {}),
|
|
3320
|
-
replyToMessageId,
|
|
3321
|
-
},
|
|
4324
|
+
Object.keys(sendOptions).length > 0 ? sendOptions : undefined,
|
|
3322
4325
|
);
|
|
4326
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3323
4327
|
return toolResult({
|
|
3324
4328
|
ok: true,
|
|
3325
|
-
replied: true,
|
|
3326
4329
|
to,
|
|
3327
|
-
|
|
4330
|
+
...(canUseNativeReply ? {
|
|
4331
|
+
replied: true,
|
|
4332
|
+
replyToMessageId,
|
|
4333
|
+
} : {}),
|
|
3328
4334
|
messageId: String(result.messageId ?? ''),
|
|
3329
4335
|
});
|
|
3330
4336
|
} catch (err) {
|
|
@@ -3394,6 +4400,7 @@ export const bitrix24Plugin = {
|
|
|
3394
4400
|
...(attach ? { attach } : {}),
|
|
3395
4401
|
},
|
|
3396
4402
|
);
|
|
4403
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3397
4404
|
return toolResult({ ok: true, edited: true, messageId });
|
|
3398
4405
|
} catch (err) {
|
|
3399
4406
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -3429,6 +4436,7 @@ export const bitrix24Plugin = {
|
|
|
3429
4436
|
|
|
3430
4437
|
try {
|
|
3431
4438
|
await api.deleteMessage(config.webhookUrl, bot, messageId, complete);
|
|
4439
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3432
4440
|
return toolResult({ ok: true, deleted: true, messageId });
|
|
3433
4441
|
} catch (err) {
|
|
3434
4442
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -3453,6 +4461,13 @@ export const bitrix24Plugin = {
|
|
|
3453
4461
|
| { currentMessageId?: string | number } | undefined;
|
|
3454
4462
|
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
3455
4463
|
const messageId = Number(rawMessageId);
|
|
4464
|
+
const reactionAckSourceMessageId = typeof rawMessageId === 'string' || typeof rawMessageId === 'number'
|
|
4465
|
+
? rawMessageId
|
|
4466
|
+
: toolContext?.currentMessageId;
|
|
4467
|
+
const reactionAckDialogId = resolveRecentInboundDialogId(
|
|
4468
|
+
reactionAckSourceMessageId,
|
|
4469
|
+
ctx.accountId,
|
|
4470
|
+
);
|
|
3456
4471
|
|
|
3457
4472
|
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
3458
4473
|
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 reactions. Do not retry.' });
|
|
@@ -3469,6 +4484,7 @@ export const bitrix24Plugin = {
|
|
|
3469
4484
|
}
|
|
3470
4485
|
try {
|
|
3471
4486
|
await api.deleteReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
4487
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3472
4488
|
return toolResult({ ok: true, removed: true });
|
|
3473
4489
|
} catch (err) {
|
|
3474
4490
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -3493,13 +4509,27 @@ export const bitrix24Plugin = {
|
|
|
3493
4509
|
|
|
3494
4510
|
try {
|
|
3495
4511
|
await api.addReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
4512
|
+
await maybeSendReactionTypingReset({
|
|
4513
|
+
sendService: gatewayState.sendService,
|
|
4514
|
+
webhookUrl: config.webhookUrl,
|
|
4515
|
+
bot,
|
|
4516
|
+
dialogId: reactionAckDialogId,
|
|
4517
|
+
});
|
|
3496
4518
|
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
4519
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3497
4520
|
return toolResult({ ok: true, added: emoji });
|
|
3498
4521
|
} catch (err) {
|
|
3499
4522
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3500
4523
|
const isAlreadySet = errMsg.includes('REACTION_ALREADY_SET');
|
|
3501
4524
|
if (isAlreadySet) {
|
|
4525
|
+
await maybeSendReactionTypingReset({
|
|
4526
|
+
sendService: gatewayState.sendService,
|
|
4527
|
+
webhookUrl: config.webhookUrl,
|
|
4528
|
+
bot,
|
|
4529
|
+
dialogId: reactionAckDialogId,
|
|
4530
|
+
});
|
|
3502
4531
|
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
4532
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3503
4533
|
return toolResult({ ok: true, added: emoji, warning: 'Reaction already set.' });
|
|
3504
4534
|
}
|
|
3505
4535
|
return toolResult({ ok: false, reason: 'error', emoji, hint: `Reaction failed: ${errMsg}. Do not retry.` });
|
|
@@ -3675,6 +4705,30 @@ export const bitrix24Plugin = {
|
|
|
3675
4705
|
|
|
3676
4706
|
const sendService = new SendService(api, logger);
|
|
3677
4707
|
const mediaService = new MediaService(api, logger);
|
|
4708
|
+
const originalSendText = sendService.sendText.bind(sendService);
|
|
4709
|
+
sendService.sendText = async (...args) => {
|
|
4710
|
+
const result = await originalSendText(...args);
|
|
4711
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
4712
|
+
return result;
|
|
4713
|
+
};
|
|
4714
|
+
const originalAnswerCommandText = sendService.answerCommandText.bind(sendService);
|
|
4715
|
+
sendService.answerCommandText = async (...args) => {
|
|
4716
|
+
const result = await originalAnswerCommandText(...args);
|
|
4717
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
4718
|
+
return result;
|
|
4719
|
+
};
|
|
4720
|
+
const originalApiSendMessage = api.sendMessage.bind(api);
|
|
4721
|
+
api.sendMessage = async (...args) => {
|
|
4722
|
+
const messageId = await originalApiSendMessage(...args);
|
|
4723
|
+
markRecentOutboundMessage(messageId, ctx.accountId);
|
|
4724
|
+
return messageId;
|
|
4725
|
+
};
|
|
4726
|
+
const originalUploadMediaToChat = mediaService.uploadMediaToChat.bind(mediaService);
|
|
4727
|
+
mediaService.uploadMediaToChat = async (...args) => {
|
|
4728
|
+
const result = await originalUploadMediaToChat(...args);
|
|
4729
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
4730
|
+
return result;
|
|
4731
|
+
};
|
|
3678
4732
|
const hydrateReplyEntry = async (
|
|
3679
4733
|
replyToMessageId: string | undefined,
|
|
3680
4734
|
): Promise<{ sender: string; body: string; messageId: string } | undefined> => {
|
|
@@ -3867,8 +4921,12 @@ export const bitrix24Plugin = {
|
|
|
3867
4921
|
query: bodyWithReply,
|
|
3868
4922
|
historyCache,
|
|
3869
4923
|
});
|
|
4924
|
+
const inlineButtonsAgentHint = buildBitrix24InlineButtonsAgentHint();
|
|
3870
4925
|
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
3871
|
-
const
|
|
4926
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ isDm: msgCtx.isDm });
|
|
4927
|
+
const nativeReplyAgentHint = canUseNativeReply
|
|
4928
|
+
? buildBitrix24NativeReplyAgentHint(msgCtx.messageId)
|
|
4929
|
+
: undefined;
|
|
3872
4930
|
const nativeForwardAgentHint = buildBitrix24NativeForwardAgentHint({
|
|
3873
4931
|
accountId: ctx.accountId,
|
|
3874
4932
|
currentBody: body,
|
|
@@ -3877,6 +4935,7 @@ export const bitrix24Plugin = {
|
|
|
3877
4935
|
historyEntries: previousEntries,
|
|
3878
4936
|
});
|
|
3879
4937
|
const bodyForAgent = [
|
|
4938
|
+
inlineButtonsAgentHint,
|
|
3880
4939
|
fileDeliveryAgentHint,
|
|
3881
4940
|
nativeReplyAgentHint,
|
|
3882
4941
|
nativeForwardAgentHint,
|
|
@@ -3895,6 +4954,7 @@ export const bitrix24Plugin = {
|
|
|
3895
4954
|
conversation.dialogId,
|
|
3896
4955
|
msgCtx.messageId,
|
|
3897
4956
|
body,
|
|
4957
|
+
msgCtx.media.map((mediaItem) => mediaItem.name),
|
|
3898
4958
|
msgCtx.timestamp ?? Date.now(),
|
|
3899
4959
|
ctx.accountId,
|
|
3900
4960
|
);
|
|
@@ -3954,14 +5014,33 @@ export const bitrix24Plugin = {
|
|
|
3954
5014
|
deliver: async (payload) => {
|
|
3955
5015
|
await replyStatusHeartbeat.stopAndWait();
|
|
3956
5016
|
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
5017
|
+
const hadRecentMediaDelivery = mediaUrls.length === 0
|
|
5018
|
+
&& hasRecentMediaDelivery(sendCtx.dialogId, msgCtx.messageId, ctx.accountId);
|
|
3957
5019
|
if (mediaUrls.length > 0) {
|
|
3958
|
-
await uploadOutboundMedia({
|
|
5020
|
+
const uploadedMessageId = await uploadOutboundMedia({
|
|
3959
5021
|
mediaService,
|
|
3960
5022
|
sendCtx,
|
|
3961
5023
|
mediaUrls,
|
|
3962
5024
|
});
|
|
5025
|
+
markRecentMediaDelivery({
|
|
5026
|
+
dialogId: sendCtx.dialogId,
|
|
5027
|
+
currentMessageId: msgCtx.messageId,
|
|
5028
|
+
mediaUrls,
|
|
5029
|
+
accountId: ctx.accountId,
|
|
5030
|
+
messageId: uploadedMessageId,
|
|
5031
|
+
});
|
|
3963
5032
|
}
|
|
3964
5033
|
if (payload.text) {
|
|
5034
|
+
if (hadRecentMediaDelivery) {
|
|
5035
|
+
replyDelivered = true;
|
|
5036
|
+
logger.debug('Suppressing trailing text after successful Bitrix24 file upload', {
|
|
5037
|
+
senderId: msgCtx.senderId,
|
|
5038
|
+
chatId: msgCtx.chatId,
|
|
5039
|
+
messageId: msgCtx.messageId,
|
|
5040
|
+
});
|
|
5041
|
+
return;
|
|
5042
|
+
}
|
|
5043
|
+
|
|
3965
5044
|
if (consumePendingNativeForwardAck(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
3966
5045
|
replyDelivered = true;
|
|
3967
5046
|
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 forward', {
|
|
@@ -4016,7 +5095,7 @@ export const bitrix24Plugin = {
|
|
|
4016
5095
|
: [];
|
|
4017
5096
|
const nativeReplyToMessageId = nativeForwardMessageId
|
|
4018
5097
|
? undefined
|
|
4019
|
-
: replyDirective.replyToMessageId;
|
|
5098
|
+
: (canUseNativeReply ? replyDirective.replyToMessageId : undefined);
|
|
4020
5099
|
const sendOptions = {
|
|
4021
5100
|
...(keyboard ? { keyboard } : {}),
|
|
4022
5101
|
...(nativeReplyToMessageId ? { replyToMessageId: nativeReplyToMessageId } : {}),
|
|
@@ -4071,25 +5150,64 @@ export const bitrix24Plugin = {
|
|
|
4071
5150
|
},
|
|
4072
5151
|
});
|
|
4073
5152
|
|
|
5153
|
+
if (!replyDelivered && hasRecentMediaDelivery(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
5154
|
+
replyDelivered = true;
|
|
5155
|
+
logger.debug('Suppressing fallback after successful Bitrix24 file upload in the same turn', {
|
|
5156
|
+
senderId: msgCtx.senderId,
|
|
5157
|
+
chatId: msgCtx.chatId,
|
|
5158
|
+
messageId: msgCtx.messageId,
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
if (!replyDelivered && hasRecentSuccessfulAction(msgCtx.messageId, ctx.accountId)) {
|
|
5163
|
+
replyDelivered = true;
|
|
5164
|
+
logger.debug('Suppressing fallback after successful Bitrix24 action in the same turn', {
|
|
5165
|
+
senderId: msgCtx.senderId,
|
|
5166
|
+
chatId: msgCtx.chatId,
|
|
5167
|
+
messageId: msgCtx.messageId,
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
const shouldWaitForEmptyReplyFallback = !replyDelivered
|
|
5172
|
+
&& isEmptyDispatchResult(dispatchResult);
|
|
5173
|
+
|
|
4074
5174
|
if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
4075
5175
|
logger.debug('Reply completed without queued final block, waiting before fallback', {
|
|
4076
5176
|
senderId: msgCtx.senderId,
|
|
4077
5177
|
chatId: msgCtx.chatId,
|
|
4078
5178
|
counts: dispatchResult.counts,
|
|
5179
|
+
fallbackDelayMs: shouldWaitForEmptyReplyFallback
|
|
5180
|
+
? EMPTY_REPLY_FALLBACK_GRACE_MS
|
|
5181
|
+
: REPLY_FALLBACK_GRACE_MS,
|
|
4079
5182
|
});
|
|
4080
|
-
|
|
5183
|
+
if (shouldWaitForEmptyReplyFallback) {
|
|
5184
|
+
await waitEmptyReplyFallbackGraceWindow();
|
|
5185
|
+
} else {
|
|
5186
|
+
await waitReplyFallbackGraceWindow();
|
|
5187
|
+
}
|
|
4081
5188
|
}
|
|
4082
5189
|
|
|
4083
|
-
if (!replyDelivered && dispatchResult
|
|
5190
|
+
if (!replyDelivered && isEmptyDispatchResult(dispatchResult)) {
|
|
5191
|
+
replyDelivered = true;
|
|
5192
|
+
logger.warn('Reply completed without any user-visible payload or tool activity; sending fallback notice', {
|
|
5193
|
+
senderId: msgCtx.senderId,
|
|
5194
|
+
chatId: msgCtx.chatId,
|
|
5195
|
+
messageId: msgCtx.messageId,
|
|
5196
|
+
counts: dispatchResult.counts,
|
|
5197
|
+
});
|
|
5198
|
+
await sendService.sendText(sendCtx, emptyReplyFallback(msgCtx.language));
|
|
5199
|
+
} else if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
4084
5200
|
logger.warn('Reply completed without a final user-visible message; fallback notice suppressed', {
|
|
4085
5201
|
senderId: msgCtx.senderId,
|
|
4086
5202
|
chatId: msgCtx.chatId,
|
|
5203
|
+
messageId: msgCtx.messageId,
|
|
4087
5204
|
counts: dispatchResult.counts,
|
|
4088
5205
|
});
|
|
4089
5206
|
} else if (replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
4090
5207
|
logger.debug('Late reply arrived during fallback grace window, skipping fallback', {
|
|
4091
5208
|
senderId: msgCtx.senderId,
|
|
4092
5209
|
chatId: msgCtx.chatId,
|
|
5210
|
+
messageId: msgCtx.messageId,
|
|
4093
5211
|
});
|
|
4094
5212
|
}
|
|
4095
5213
|
} catch (err) {
|
|
@@ -4106,7 +5224,9 @@ export const bitrix24Plugin = {
|
|
|
4106
5224
|
});
|
|
4107
5225
|
}
|
|
4108
5226
|
} finally {
|
|
4109
|
-
|
|
5227
|
+
mediaService.scheduleDownloadedMediaCleanup(
|
|
5228
|
+
downloadedMedia.map((mediaItem) => mediaItem.path),
|
|
5229
|
+
);
|
|
4110
5230
|
}
|
|
4111
5231
|
};
|
|
4112
5232
|
const directTextCoalescer = new BufferedDirectMessageCoalescer({
|
|
@@ -4298,6 +5418,16 @@ export const bitrix24Plugin = {
|
|
|
4298
5418
|
textLen: msgCtx.text.length,
|
|
4299
5419
|
});
|
|
4300
5420
|
|
|
5421
|
+
if (hasRecentOutboundMessage(msgCtx.messageId, ctx.accountId)) {
|
|
5422
|
+
logger.debug('Skipping recent outbound Bitrix24 message echo', {
|
|
5423
|
+
senderId: msgCtx.senderId,
|
|
5424
|
+
chatId: msgCtx.chatId,
|
|
5425
|
+
messageId: msgCtx.messageId,
|
|
5426
|
+
eventScope: msgCtx.eventScope,
|
|
5427
|
+
});
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5430
|
+
|
|
4301
5431
|
const pendingForwardContext = msgCtx.isForwarded
|
|
4302
5432
|
? directTextCoalescer.take(ctx.accountId, msgCtx.chatId)
|
|
4303
5433
|
: null;
|
|
@@ -4324,7 +5454,7 @@ export const bitrix24Plugin = {
|
|
|
4324
5454
|
chatId: msgCtx.chatInternalId,
|
|
4325
5455
|
})
|
|
4326
5456
|
: null;
|
|
4327
|
-
const agentWatchRules = config.agentMode
|
|
5457
|
+
const agentWatchRules = config.agentMode
|
|
4328
5458
|
? resolveAgentWatchRules({
|
|
4329
5459
|
config,
|
|
4330
5460
|
dialogId: msgCtx.chatId,
|
|
@@ -4334,43 +5464,71 @@ export const bitrix24Plugin = {
|
|
|
4334
5464
|
const watchRule = msgCtx.isGroup && groupAccess?.groupAllowed
|
|
4335
5465
|
? findMatchingWatchRule(msgCtx, groupAccess?.watch)
|
|
4336
5466
|
: undefined;
|
|
4337
|
-
const
|
|
5467
|
+
const botId = String(bot.botId);
|
|
5468
|
+
const ownerBotDmParticipantIds = new Set(
|
|
5469
|
+
[msgCtx.senderId, msgCtx.chatId, msgCtx.chatInternalId]
|
|
5470
|
+
.map((value) => String(value ?? '').trim())
|
|
5471
|
+
.filter(Boolean),
|
|
5472
|
+
);
|
|
5473
|
+
const isOwnerBotUserDmMirror = Boolean(
|
|
5474
|
+
config.agentMode
|
|
5475
|
+
&& msgCtx.eventScope === 'user'
|
|
5476
|
+
&& msgCtx.isDm
|
|
4338
5477
|
&& webhookOwnerId
|
|
4339
|
-
&&
|
|
5478
|
+
&& ownerBotDmParticipantIds.has(webhookOwnerId)
|
|
5479
|
+
&& ownerBotDmParticipantIds.has(botId)
|
|
5480
|
+
);
|
|
5481
|
+
const isOwnerAuthoredMessage = Boolean(
|
|
5482
|
+
webhookOwnerId && msgCtx.senderId === webhookOwnerId
|
|
5483
|
+
);
|
|
5484
|
+
const isCurrentBotAuthoredMessage = msgCtx.senderId === botId;
|
|
5485
|
+
const activeWatchRule = watchRule?.mode === 'notifyOwnerDm'
|
|
5486
|
+
&& (isOwnerAuthoredMessage || isCurrentBotAuthoredMessage)
|
|
4340
5487
|
? undefined
|
|
4341
5488
|
: watchRule;
|
|
4342
|
-
const agentWatchRule = msgCtx
|
|
4343
|
-
|
|
5489
|
+
const agentWatchRule = findMatchingWatchRule(msgCtx, agentWatchRules);
|
|
5490
|
+
const activeAgentWatchRule = agentWatchRule?.mode === 'notifyOwnerDm'
|
|
5491
|
+
&& !isOwnerBotUserDmMirror
|
|
5492
|
+
&& !isOwnerAuthoredMessage
|
|
5493
|
+
&& !isCurrentBotAuthoredMessage
|
|
5494
|
+
? agentWatchRule
|
|
4344
5495
|
: undefined;
|
|
4345
5496
|
|
|
4346
5497
|
/** Shorthand: record message in RAM history for this dialog. */
|
|
4347
5498
|
const recordHistory = (body?: string) =>
|
|
4348
5499
|
appendMessageToHistory({ historyCache, historyKey, historyLimit, msgCtx, body });
|
|
4349
5500
|
|
|
4350
|
-
if (
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
});
|
|
4362
|
-
return;
|
|
4363
|
-
}
|
|
5501
|
+
if (isOwnerBotUserDmMirror) {
|
|
5502
|
+
logger.debug('Skipping mirrored agent-mode owner DM user event; bot channel will handle it', {
|
|
5503
|
+
senderId: msgCtx.senderId,
|
|
5504
|
+
chatId: msgCtx.chatId,
|
|
5505
|
+
messageId: msgCtx.messageId,
|
|
5506
|
+
eventScope: msgCtx.eventScope,
|
|
5507
|
+
webhookOwnerId,
|
|
5508
|
+
botId,
|
|
5509
|
+
});
|
|
5510
|
+
return;
|
|
5511
|
+
}
|
|
4364
5512
|
|
|
5513
|
+
if (msgCtx.eventScope === 'user') {
|
|
4365
5514
|
recordHistory();
|
|
4366
5515
|
|
|
4367
|
-
if (
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
5516
|
+
if (activeAgentWatchRule) {
|
|
5517
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
5518
|
+
logger.debug('Skipping duplicate agent watch notification for recent message', {
|
|
5519
|
+
senderId: msgCtx.senderId,
|
|
5520
|
+
chatId: msgCtx.chatId,
|
|
5521
|
+
messageId: msgCtx.messageId,
|
|
5522
|
+
eventScope: msgCtx.eventScope,
|
|
5523
|
+
});
|
|
5524
|
+
} else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeAgentWatchRule)) {
|
|
5525
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
5526
|
+
logger.debug('User-event watch matched and notified webhook owner in DM', {
|
|
5527
|
+
senderId: msgCtx.senderId,
|
|
5528
|
+
chatId: msgCtx.chatId,
|
|
5529
|
+
messageId: msgCtx.messageId,
|
|
5530
|
+
});
|
|
5531
|
+
}
|
|
4374
5532
|
}
|
|
4375
5533
|
return;
|
|
4376
5534
|
}
|
|
@@ -4395,6 +5553,7 @@ export const bitrix24Plugin = {
|
|
|
4395
5553
|
if (
|
|
4396
5554
|
msgCtx.isGroup
|
|
4397
5555
|
&& !activeWatchRule
|
|
5556
|
+
&& !activeAgentWatchRule
|
|
4398
5557
|
&& groupAccess?.requireMention
|
|
4399
5558
|
&& !msgCtx.wasMentioned
|
|
4400
5559
|
) {
|
|
@@ -4411,12 +5570,42 @@ export const bitrix24Plugin = {
|
|
|
4411
5570
|
if (activeWatchRule?.mode === 'notifyOwnerDm') {
|
|
4412
5571
|
recordHistory();
|
|
4413
5572
|
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
5573
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
5574
|
+
logger.debug('Skipping duplicate group watch notification for recent message', {
|
|
5575
|
+
senderId: msgCtx.senderId,
|
|
5576
|
+
chatId: msgCtx.chatId,
|
|
5577
|
+
messageId: msgCtx.messageId,
|
|
5578
|
+
eventScope: msgCtx.eventScope,
|
|
5579
|
+
});
|
|
5580
|
+
} else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeWatchRule)) {
|
|
5581
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
5582
|
+
logger.debug('Group watch matched and notified webhook owner in DM', {
|
|
5583
|
+
senderId: msgCtx.senderId,
|
|
5584
|
+
chatId: msgCtx.chatId,
|
|
5585
|
+
messageId: msgCtx.messageId,
|
|
5586
|
+
});
|
|
5587
|
+
}
|
|
5588
|
+
return;
|
|
5589
|
+
}
|
|
5590
|
+
|
|
5591
|
+
if (activeAgentWatchRule) {
|
|
5592
|
+
recordHistory();
|
|
5593
|
+
|
|
5594
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
5595
|
+
logger.debug('Skipping duplicate bot-scope agent watch notification for recent message', {
|
|
5596
|
+
senderId: msgCtx.senderId,
|
|
5597
|
+
chatId: msgCtx.chatId,
|
|
5598
|
+
messageId: msgCtx.messageId,
|
|
5599
|
+
eventScope: msgCtx.eventScope,
|
|
5600
|
+
});
|
|
5601
|
+
} else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeAgentWatchRule)) {
|
|
5602
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
5603
|
+
logger.debug('Bot-scope agent watch matched and notified webhook owner in DM', {
|
|
5604
|
+
senderId: msgCtx.senderId,
|
|
5605
|
+
chatId: msgCtx.chatId,
|
|
5606
|
+
messageId: msgCtx.messageId,
|
|
5607
|
+
});
|
|
5608
|
+
}
|
|
4420
5609
|
return;
|
|
4421
5610
|
}
|
|
4422
5611
|
|
|
@@ -4729,9 +5918,14 @@ export const bitrix24Plugin = {
|
|
|
4729
5918
|
accountId: ctx.accountId,
|
|
4730
5919
|
peer: conversation.peer,
|
|
4731
5920
|
});
|
|
5921
|
+
const inlineButtonsAgentHint = buildBitrix24InlineButtonsAgentHint();
|
|
4732
5922
|
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
4733
|
-
const
|
|
5923
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ isDm });
|
|
5924
|
+
const nativeReplyAgentHint = canUseNativeReply
|
|
5925
|
+
? buildBitrix24NativeReplyAgentHint(commandMessageId)
|
|
5926
|
+
: undefined;
|
|
4734
5927
|
const commandBodyForAgent = [
|
|
5928
|
+
inlineButtonsAgentHint,
|
|
4735
5929
|
fileDeliveryAgentHint,
|
|
4736
5930
|
nativeReplyAgentHint,
|
|
4737
5931
|
commandText,
|
|
@@ -4797,10 +5991,12 @@ export const bitrix24Plugin = {
|
|
|
4797
5991
|
const sendOptions = {
|
|
4798
5992
|
keyboard,
|
|
4799
5993
|
convertMarkdown: formattedPayload.convertMarkdown,
|
|
4800
|
-
...(
|
|
5994
|
+
...(canUseNativeReply && replyDirective.replyToMessageId
|
|
5995
|
+
? { replyToMessageId: replyDirective.replyToMessageId }
|
|
5996
|
+
: {}),
|
|
4801
5997
|
};
|
|
4802
5998
|
if (!commandReplyDelivered) {
|
|
4803
|
-
if (isDm || replyDirective.replyToMessageId) {
|
|
5999
|
+
if (isDm || (canUseNativeReply && replyDirective.replyToMessageId)) {
|
|
4804
6000
|
commandReplyDelivered = true;
|
|
4805
6001
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
4806
6002
|
...sendOptions,
|
|
@@ -4828,21 +6024,69 @@ export const bitrix24Plugin = {
|
|
|
4828
6024
|
},
|
|
4829
6025
|
});
|
|
4830
6026
|
|
|
6027
|
+
if (!commandReplyDelivered && hasRecentMediaDelivery(sendCtx.dialogId, commandMessageId, ctx.accountId)) {
|
|
6028
|
+
commandReplyDelivered = true;
|
|
6029
|
+
logger.debug('Suppressing command fallback after successful Bitrix24 file upload in the same turn', {
|
|
6030
|
+
commandName,
|
|
6031
|
+
senderId,
|
|
6032
|
+
dialogId,
|
|
6033
|
+
messageId: commandMessageId,
|
|
6034
|
+
});
|
|
6035
|
+
}
|
|
6036
|
+
|
|
6037
|
+
if (!commandReplyDelivered && hasRecentSuccessfulAction(commandMessageId, ctx.accountId)) {
|
|
6038
|
+
commandReplyDelivered = true;
|
|
6039
|
+
logger.debug('Suppressing command fallback after successful Bitrix24 action in the same turn', {
|
|
6040
|
+
commandName,
|
|
6041
|
+
senderId,
|
|
6042
|
+
dialogId,
|
|
6043
|
+
messageId: commandMessageId,
|
|
6044
|
+
});
|
|
6045
|
+
}
|
|
6046
|
+
|
|
6047
|
+
const shouldWaitForEmptyCommandReplyFallback = !commandReplyDelivered
|
|
6048
|
+
&& isEmptyDispatchResult(dispatchResult);
|
|
6049
|
+
|
|
4831
6050
|
if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
4832
6051
|
logger.debug('Command reply completed without queued final block, waiting before fallback', {
|
|
4833
6052
|
commandName,
|
|
4834
6053
|
senderId,
|
|
4835
6054
|
dialogId,
|
|
4836
6055
|
counts: dispatchResult.counts,
|
|
6056
|
+
fallbackDelayMs: shouldWaitForEmptyCommandReplyFallback
|
|
6057
|
+
? EMPTY_REPLY_FALLBACK_GRACE_MS
|
|
6058
|
+
: REPLY_FALLBACK_GRACE_MS,
|
|
4837
6059
|
});
|
|
4838
|
-
|
|
6060
|
+
if (shouldWaitForEmptyCommandReplyFallback) {
|
|
6061
|
+
await waitEmptyReplyFallbackGraceWindow();
|
|
6062
|
+
} else {
|
|
6063
|
+
await waitReplyFallbackGraceWindow();
|
|
6064
|
+
}
|
|
4839
6065
|
}
|
|
4840
6066
|
|
|
4841
|
-
if (!commandReplyDelivered && dispatchResult
|
|
6067
|
+
if (!commandReplyDelivered && isEmptyDispatchResult(dispatchResult)) {
|
|
6068
|
+
commandReplyDelivered = true;
|
|
6069
|
+
logger.warn('Command reply completed without any user-visible payload or tool activity; sending fallback notice', {
|
|
6070
|
+
commandName,
|
|
6071
|
+
senderId,
|
|
6072
|
+
dialogId,
|
|
6073
|
+
messageId: commandMessageId,
|
|
6074
|
+
counts: dispatchResult.counts,
|
|
6075
|
+
});
|
|
6076
|
+
const fallbackText = emptyReplyFallback(cmdCtx.language);
|
|
6077
|
+
if (isDm) {
|
|
6078
|
+
await sendService.sendText(sendCtx, fallbackText);
|
|
6079
|
+
} else {
|
|
6080
|
+
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
6081
|
+
keyboard: defaultCommandKeyboard,
|
|
6082
|
+
});
|
|
6083
|
+
}
|
|
6084
|
+
} else if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
4842
6085
|
logger.warn('Command reply completed without a final user-visible message; fallback notice suppressed', {
|
|
4843
6086
|
commandName,
|
|
4844
6087
|
senderId,
|
|
4845
6088
|
dialogId,
|
|
6089
|
+
messageId: commandMessageId,
|
|
4846
6090
|
counts: dispatchResult.counts,
|
|
4847
6091
|
});
|
|
4848
6092
|
} else if (commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
@@ -4850,6 +6094,7 @@ export const bitrix24Plugin = {
|
|
|
4850
6094
|
commandName,
|
|
4851
6095
|
senderId,
|
|
4852
6096
|
dialogId,
|
|
6097
|
+
messageId: commandMessageId,
|
|
4853
6098
|
});
|
|
4854
6099
|
}
|
|
4855
6100
|
} catch (err) {
|