@ihazz/bitrix24 1.1.11 → 1.1.13
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/README.md +77 -4
- package/dist/src/api.d.ts +10 -5
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +42 -8
- package/dist/src/api.js.map +1 -1
- package/dist/src/channel.d.ts +18 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +1274 -71
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.d.ts +0 -1
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +68 -79
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/inbound-handler.js +85 -7
- 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 +117 -14
- 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 +73 -3
- package/dist/src/message-utils.js.map +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/send-service.d.ts +1 -0
- package/dist/src/send-service.d.ts.map +1 -1
- package/dist/src/send-service.js +26 -3
- package/dist/src/send-service.js.map +1 -1
- package/dist/src/state-paths.d.ts +1 -0
- package/dist/src/state-paths.d.ts.map +1 -1
- package/dist/src/state-paths.js +9 -0
- package/dist/src/state-paths.js.map +1 -1
- package/dist/src/types.d.ts +92 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +62 -13
- package/src/channel.ts +1739 -96
- package/src/i18n.ts +68 -81
- package/src/inbound-handler.ts +110 -7
- package/src/media-service.ts +146 -15
- package/src/message-utils.ts +90 -3
- package/src/runtime.ts +1 -0
- package/src/send-service.ts +40 -2
- package/src/state-paths.ts +11 -0
- package/src/types.ts +122 -0
package/src/channel.ts
CHANGED
|
@@ -48,13 +48,15 @@ import {
|
|
|
48
48
|
normalizeNewSessionReply,
|
|
49
49
|
ownerAndAllowedUsersOnly,
|
|
50
50
|
personalBotOwnerOnly,
|
|
51
|
-
replyGenerationFailed,
|
|
52
51
|
welcomeKeyboardLabels,
|
|
53
52
|
watchOwnerDmNotice,
|
|
54
53
|
} from './i18n.js';
|
|
55
54
|
import { HistoryCache } from './history-cache.js';
|
|
56
|
-
import type { ConversationMeta } from './history-cache.js';
|
|
55
|
+
import type { ConversationMeta, HistoryEntry } from './history-cache.js';
|
|
57
56
|
import type {
|
|
57
|
+
B24Attach,
|
|
58
|
+
B24AttachBlock,
|
|
59
|
+
B24AttachColorToken,
|
|
58
60
|
B24MsgContext,
|
|
59
61
|
B24InputActionStatusCode,
|
|
60
62
|
B24V2FetchEventItem,
|
|
@@ -69,9 +71,12 @@ import type {
|
|
|
69
71
|
B24V2GetMessageResult,
|
|
70
72
|
B24V2User,
|
|
71
73
|
B24Keyboard,
|
|
74
|
+
ChannelMessageToolDiscovery,
|
|
75
|
+
ExtractedToolSendTarget,
|
|
72
76
|
KeyboardButton,
|
|
73
77
|
Logger,
|
|
74
78
|
} from './types.js';
|
|
79
|
+
import { markdownToBbCode } from './message-utils.js';
|
|
75
80
|
|
|
76
81
|
const PHASE_STATUS_DURATION_SECONDS = 8;
|
|
77
82
|
const PHASE_STATUS_REFRESH_GRACE_MS = 1000;
|
|
@@ -94,7 +99,24 @@ const FORWARDED_CONTEXT_RANGE = 5;
|
|
|
94
99
|
const ACTIVE_SESSION_NAMESPACE_TTL_MS = 180 * 24 * 60 * 60 * 1000;
|
|
95
100
|
const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
|
|
96
101
|
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
|
+
const NATIVE_FORWARD_ACK_TTL_MS = 60 * 1000;
|
|
103
|
+
const NATIVE_REACTION_ACK_TTL_MS = 60 * 1000;
|
|
104
|
+
const RECENT_INBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
105
|
+
const RECENT_INBOUND_MESSAGE_LIMIT = 20;
|
|
97
106
|
const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
|
|
107
|
+
const pendingNativeForwardAcks = new Map<string, number>();
|
|
108
|
+
const pendingNativeReactionAcks = new Map<string, { emoji: string; expiresAt: number }>();
|
|
109
|
+
const recentInboundMessagesByDialog = new Map<string, Array<{
|
|
110
|
+
messageId: string;
|
|
111
|
+
body: string;
|
|
112
|
+
timestamp: number;
|
|
113
|
+
}>>();
|
|
114
|
+
const inboundMessageContextById = new Map<string, {
|
|
115
|
+
dialogId: string;
|
|
116
|
+
dialogKey: string;
|
|
117
|
+
body: string;
|
|
118
|
+
timestamp: number;
|
|
119
|
+
}>();
|
|
98
120
|
|
|
99
121
|
// ─── Emoji → B24 reaction code mapping ──────────────────────────────────
|
|
100
122
|
// B24 uses named reaction codes, not Unicode emoji.
|
|
@@ -170,6 +192,606 @@ function toMessageId(value: string | number | undefined): number | undefined {
|
|
|
170
192
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
171
193
|
}
|
|
172
194
|
|
|
195
|
+
function resolveStateAccountId(accountId?: string): string {
|
|
196
|
+
const normalizedAccountId = typeof accountId === 'string'
|
|
197
|
+
? accountId.trim()
|
|
198
|
+
: '';
|
|
199
|
+
if (normalizedAccountId) {
|
|
200
|
+
return normalizedAccountId;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const gatewayAccountId = gatewayState?.accountId?.trim();
|
|
204
|
+
return gatewayAccountId || 'default';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildAccountDialogScopeKey(accountId: string | undefined, dialogId: string): string | undefined {
|
|
208
|
+
const normalizedDialogId = dialogId.trim();
|
|
209
|
+
if (!normalizedDialogId) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return `${resolveStateAccountId(accountId)}:${normalizedDialogId}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildAccountMessageScopeKey(
|
|
217
|
+
accountId: string | undefined,
|
|
218
|
+
currentMessageId: string | number | undefined,
|
|
219
|
+
): string | undefined {
|
|
220
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
221
|
+
if (!normalizedMessageId) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return `${resolveStateAccountId(accountId)}:${normalizedMessageId}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildPendingNativeForwardAckKey(
|
|
229
|
+
dialogId: string,
|
|
230
|
+
currentMessageId: string | number | undefined,
|
|
231
|
+
accountId?: string,
|
|
232
|
+
): string | undefined {
|
|
233
|
+
const dialogKey = buildAccountDialogScopeKey(accountId, dialogId);
|
|
234
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
235
|
+
if (!dialogKey || !normalizedMessageId) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return `${dialogKey}:${normalizedMessageId}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function prunePendingNativeForwardAcks(now = Date.now()): void {
|
|
243
|
+
for (const [key, expiresAt] of pendingNativeForwardAcks.entries()) {
|
|
244
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
245
|
+
pendingNativeForwardAcks.delete(key);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function markPendingNativeForwardAck(
|
|
251
|
+
dialogId: string,
|
|
252
|
+
currentMessageId: string | number | undefined,
|
|
253
|
+
accountId?: string,
|
|
254
|
+
): void {
|
|
255
|
+
const key = buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
256
|
+
if (!key) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
prunePendingNativeForwardAcks();
|
|
261
|
+
pendingNativeForwardAcks.set(key, Date.now() + NATIVE_FORWARD_ACK_TTL_MS);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function consumePendingNativeForwardAck(
|
|
265
|
+
dialogId: string,
|
|
266
|
+
currentMessageId: string | number | undefined,
|
|
267
|
+
accountId?: string,
|
|
268
|
+
): boolean {
|
|
269
|
+
const key = buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
270
|
+
if (!key) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
prunePendingNativeForwardAcks();
|
|
275
|
+
const expiresAt = pendingNativeForwardAcks.get(key);
|
|
276
|
+
if (!expiresAt) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
pendingNativeForwardAcks.delete(key);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function resolvePendingNativeForwardAckDialogId(
|
|
285
|
+
currentMessageId: string | number | undefined,
|
|
286
|
+
fallbackDialogId: string,
|
|
287
|
+
accountId?: string,
|
|
288
|
+
): string {
|
|
289
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
290
|
+
if (!messageKey) {
|
|
291
|
+
return fallbackDialogId;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return inboundMessageContextById.get(messageKey)?.dialogId ?? fallbackDialogId;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildPendingNativeReactionAckKey(
|
|
298
|
+
currentMessageId: string | number | undefined,
|
|
299
|
+
accountId?: string,
|
|
300
|
+
): string | undefined {
|
|
301
|
+
return buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function prunePendingNativeReactionAcks(now = Date.now()): void {
|
|
305
|
+
for (const [key, entry] of pendingNativeReactionAcks.entries()) {
|
|
306
|
+
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
307
|
+
pendingNativeReactionAcks.delete(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function markPendingNativeReactionAck(
|
|
313
|
+
currentMessageId: string | number | undefined,
|
|
314
|
+
emoji: string,
|
|
315
|
+
accountId?: string,
|
|
316
|
+
): void {
|
|
317
|
+
const key = buildPendingNativeReactionAckKey(currentMessageId, accountId);
|
|
318
|
+
const normalizedEmoji = emoji.trim();
|
|
319
|
+
if (!key || !normalizedEmoji) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
prunePendingNativeReactionAcks();
|
|
324
|
+
pendingNativeReactionAcks.set(key, {
|
|
325
|
+
emoji: normalizedEmoji,
|
|
326
|
+
expiresAt: Date.now() + NATIVE_REACTION_ACK_TTL_MS,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function consumePendingNativeReactionAck(
|
|
331
|
+
currentMessageId: string | number | undefined,
|
|
332
|
+
text: string,
|
|
333
|
+
accountId?: string,
|
|
334
|
+
): boolean {
|
|
335
|
+
const key = buildPendingNativeReactionAckKey(currentMessageId, accountId);
|
|
336
|
+
if (!key) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
prunePendingNativeReactionAcks();
|
|
341
|
+
const pendingAck = pendingNativeReactionAcks.get(key);
|
|
342
|
+
if (!pendingAck) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (normalizeComparableMessageText(text) !== pendingAck.emoji) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
pendingNativeReactionAcks.delete(key);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function pruneRecentInboundMessages(now = Date.now()): void {
|
|
355
|
+
for (const [dialogKey, entries] of recentInboundMessagesByDialog.entries()) {
|
|
356
|
+
const nextEntries = entries.filter((entry) => Number.isFinite(entry.timestamp) && entry.timestamp > now - RECENT_INBOUND_MESSAGE_TTL_MS);
|
|
357
|
+
if (nextEntries.length === 0) {
|
|
358
|
+
recentInboundMessagesByDialog.delete(dialogKey);
|
|
359
|
+
} else if (nextEntries.length !== entries.length) {
|
|
360
|
+
recentInboundMessagesByDialog.set(dialogKey, nextEntries);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (const [messageId, entry] of inboundMessageContextById.entries()) {
|
|
365
|
+
if (!Number.isFinite(entry.timestamp) || entry.timestamp <= now - RECENT_INBOUND_MESSAGE_TTL_MS) {
|
|
366
|
+
inboundMessageContextById.delete(messageId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function rememberRecentInboundMessage(
|
|
372
|
+
dialogId: string,
|
|
373
|
+
messageId: string | number | undefined,
|
|
374
|
+
body: string,
|
|
375
|
+
timestamp = Date.now(),
|
|
376
|
+
accountId?: string,
|
|
377
|
+
): void {
|
|
378
|
+
const normalizedMessageId = toMessageId(messageId);
|
|
379
|
+
const normalizedBody = body.trim();
|
|
380
|
+
const dialogKey = buildAccountDialogScopeKey(accountId, dialogId);
|
|
381
|
+
const messageKey = buildAccountMessageScopeKey(accountId, normalizedMessageId);
|
|
382
|
+
if (!dialogKey || !messageKey || !normalizedBody) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
pruneRecentInboundMessages(timestamp);
|
|
387
|
+
|
|
388
|
+
const id = String(normalizedMessageId);
|
|
389
|
+
const currentEntries = recentInboundMessagesByDialog.get(dialogKey) ?? [];
|
|
390
|
+
const nextEntries = currentEntries
|
|
391
|
+
.filter((entry) => entry.messageId !== id)
|
|
392
|
+
.concat({ messageId: id, body: normalizedBody, timestamp })
|
|
393
|
+
.slice(-RECENT_INBOUND_MESSAGE_LIMIT);
|
|
394
|
+
|
|
395
|
+
recentInboundMessagesByDialog.set(dialogKey, nextEntries);
|
|
396
|
+
inboundMessageContextById.set(messageKey, {
|
|
397
|
+
dialogId,
|
|
398
|
+
dialogKey,
|
|
399
|
+
body: normalizedBody,
|
|
400
|
+
timestamp,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function findPreviousRecentInboundMessage(
|
|
405
|
+
currentMessageId: string | number | undefined,
|
|
406
|
+
accountId?: string,
|
|
407
|
+
): { messageId: string; body: string } | undefined {
|
|
408
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
409
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
410
|
+
if (!messageKey || !normalizedMessageId) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
pruneRecentInboundMessages();
|
|
415
|
+
|
|
416
|
+
const currentEntry = inboundMessageContextById.get(messageKey);
|
|
417
|
+
if (!currentEntry) {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const dialogEntries = recentInboundMessagesByDialog.get(currentEntry.dialogKey);
|
|
422
|
+
if (!dialogEntries || dialogEntries.length === 0) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const currentIndex = dialogEntries.findIndex((entry) => entry.messageId === String(normalizedMessageId));
|
|
427
|
+
if (currentIndex <= 0) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for (let index = currentIndex - 1; index >= 0; index -= 1) {
|
|
432
|
+
const entry = dialogEntries[index];
|
|
433
|
+
if (entry.body.trim()) {
|
|
434
|
+
return entry;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const INLINE_REPLY_DIRECTIVE_RE = /\[\[\s*(reply_to_current|reply_to\s*:\s*([^\]\n]+))\s*\]\]/gi;
|
|
442
|
+
|
|
443
|
+
function normalizeComparableMessageText(value: string): string {
|
|
444
|
+
return value
|
|
445
|
+
.replace(/\s+/g, ' ')
|
|
446
|
+
.trim();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeBitrix24DialogTarget(raw: string): string {
|
|
450
|
+
const stripped = raw.trim().replace(CHANNEL_PREFIX_RE, '');
|
|
451
|
+
if (!stripped) {
|
|
452
|
+
return '';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const bracketedUserMatch = stripped.match(/^\[USER=(\d+)(?:[^\]]*)\][\s\S]*?\[\/USER\]$/iu);
|
|
456
|
+
if (bracketedUserMatch?.[1]) {
|
|
457
|
+
return bracketedUserMatch[1];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const bracketedChatMatch = stripped.match(/^\[CHAT=(?:chat)?(\d+)(?:[^\]]*)\][\s\S]*?\[\/CHAT\]$/iu);
|
|
461
|
+
if (bracketedChatMatch?.[1]) {
|
|
462
|
+
return `chat${bracketedChatMatch[1]}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const plainUserMatch = stripped.match(/^USER=(\d+)$/iu);
|
|
466
|
+
if (plainUserMatch?.[1]) {
|
|
467
|
+
return plainUserMatch[1];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const plainChatMatch = stripped.match(/^CHAT=(?:chat)?(\d+)$/iu);
|
|
471
|
+
if (plainChatMatch?.[1]) {
|
|
472
|
+
return `chat${plainChatMatch[1]}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const prefixedChatMatch = stripped.match(/^chat(\d+)$/iu);
|
|
476
|
+
if (prefixedChatMatch?.[1]) {
|
|
477
|
+
return `chat${prefixedChatMatch[1]}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return stripped;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function looksLikeBitrix24DialogTarget(raw: string): boolean {
|
|
484
|
+
const normalized = normalizeBitrix24DialogTarget(raw);
|
|
485
|
+
return /^\d+$/.test(normalized) || /^chat\d+$/iu.test(normalized);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function extractBitrix24DialogTargetsFromText(text: string): string[] {
|
|
489
|
+
const targets = new Set<string>();
|
|
490
|
+
|
|
491
|
+
for (const match of text.matchAll(/\[CHAT=(?:chat)?(\d+)(?:[^\]]*)\][\s\S]*?\[\/CHAT\]/giu)) {
|
|
492
|
+
if (match[1]) {
|
|
493
|
+
targets.add(`chat${match[1]}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
for (const match of text.matchAll(/\[USER=(\d+)(?:[^\]]*)\][\s\S]*?\[\/USER\]/giu)) {
|
|
498
|
+
if (match[1]) {
|
|
499
|
+
targets.add(match[1]);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const match of text.matchAll(/\bCHAT=(?:chat)?(\d+)\b/giu)) {
|
|
504
|
+
if (match[1]) {
|
|
505
|
+
targets.add(`chat${match[1]}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const match of text.matchAll(/\bUSER=(\d+)\b/giu)) {
|
|
510
|
+
if (match[1]) {
|
|
511
|
+
targets.add(match[1]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return [...targets];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function isLikelyForwardControlMessage(text: string): boolean {
|
|
519
|
+
return extractBitrix24DialogTargetsFromText(text).length > 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function extractReplyDirective(params: {
|
|
523
|
+
text: string;
|
|
524
|
+
currentMessageId?: string | number;
|
|
525
|
+
explicitReplyToId?: unknown;
|
|
526
|
+
}): { cleanText: string; replyToMessageId?: number } {
|
|
527
|
+
let wantsCurrentReply = false;
|
|
528
|
+
let inlineReplyToId: string | undefined;
|
|
529
|
+
|
|
530
|
+
const cleanText = cleanupTextAfterInlineMediaExtraction(
|
|
531
|
+
params.text.replace(INLINE_REPLY_DIRECTIVE_RE, (_match: string, _rawDirective: string, explicitId?: string) => {
|
|
532
|
+
if (typeof explicitId === 'string' && explicitId.trim()) {
|
|
533
|
+
inlineReplyToId = explicitId.trim();
|
|
534
|
+
} else {
|
|
535
|
+
wantsCurrentReply = true;
|
|
536
|
+
}
|
|
537
|
+
return ' ';
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const explicitReplyToMessageId = parseActionMessageIds(params.explicitReplyToId)[0];
|
|
542
|
+
if (explicitReplyToMessageId) {
|
|
543
|
+
return { cleanText, replyToMessageId: explicitReplyToMessageId };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const inlineReplyToMessageId = parseActionMessageIds(inlineReplyToId)[0];
|
|
547
|
+
if (inlineReplyToMessageId) {
|
|
548
|
+
return { cleanText, replyToMessageId: inlineReplyToMessageId };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (wantsCurrentReply) {
|
|
552
|
+
const currentReplyToMessageId = parseActionMessageIds(params.currentMessageId)[0];
|
|
553
|
+
if (currentReplyToMessageId) {
|
|
554
|
+
return { cleanText, replyToMessageId: currentReplyToMessageId };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return { cleanText };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function findNativeForwardTarget(params: {
|
|
562
|
+
accountId?: string;
|
|
563
|
+
requestText: string;
|
|
564
|
+
deliveredText: string;
|
|
565
|
+
historyEntries: HistoryEntry[];
|
|
566
|
+
currentMessageId?: string | number;
|
|
567
|
+
}): number | undefined {
|
|
568
|
+
const deliveredText = normalizeComparableMessageText(params.deliveredText);
|
|
569
|
+
if (!deliveredText) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (deliveredText === normalizeComparableMessageText(params.requestText)) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const currentMessageId = toMessageId(params.currentMessageId);
|
|
578
|
+
|
|
579
|
+
const matchedEntry = [...params.historyEntries]
|
|
580
|
+
.reverse()
|
|
581
|
+
.find((entry) => {
|
|
582
|
+
if (normalizeComparableMessageText(entry.body) !== deliveredText) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return currentMessageId === undefined || toMessageId(entry.messageId) !== currentMessageId;
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (!matchedEntry) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return toMessageId(matchedEntry.messageId);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function findImplicitForwardTargetFromRecentInbound(params: {
|
|
597
|
+
accountId?: string;
|
|
598
|
+
requestText: string;
|
|
599
|
+
deliveredText: string;
|
|
600
|
+
currentMessageId?: string | number;
|
|
601
|
+
historyEntries?: HistoryEntry[];
|
|
602
|
+
}): number | undefined {
|
|
603
|
+
const normalizedDeliveredText = normalizeComparableMessageText(params.deliveredText);
|
|
604
|
+
const normalizedRequestText = normalizeComparableMessageText(params.requestText);
|
|
605
|
+
const targetDialogIds = extractBitrix24DialogTargetsFromText(params.requestText);
|
|
606
|
+
const isForwardMarkerOnly = Boolean(normalizedDeliveredText) && FORWARD_MARKER_RE.test(normalizedDeliveredText);
|
|
607
|
+
|
|
608
|
+
if (
|
|
609
|
+
!normalizedDeliveredText
|
|
610
|
+
|| (
|
|
611
|
+
!isForwardMarkerOnly
|
|
612
|
+
&& (
|
|
613
|
+
targetDialogIds.length === 0
|
|
614
|
+
|| normalizedDeliveredText !== normalizedRequestText
|
|
615
|
+
)
|
|
616
|
+
)
|
|
617
|
+
) {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(
|
|
622
|
+
params.currentMessageId,
|
|
623
|
+
params.accountId,
|
|
624
|
+
);
|
|
625
|
+
if (previousInboundMessage) {
|
|
626
|
+
return Number(previousInboundMessage.messageId);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const previousHistoryEntry = [...(params.historyEntries ?? [])]
|
|
630
|
+
.reverse()
|
|
631
|
+
.find((entry) => toMessageId(entry.messageId));
|
|
632
|
+
return toMessageId(previousHistoryEntry?.messageId);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function findPreviousForwardableMessageId(params: {
|
|
636
|
+
accountId?: string;
|
|
637
|
+
currentBody: string;
|
|
638
|
+
currentMessageId?: string | number;
|
|
639
|
+
replyToMessageId?: string | number;
|
|
640
|
+
historyEntries?: HistoryEntry[];
|
|
641
|
+
}): number | undefined {
|
|
642
|
+
// If the user replied to a specific message, that's the source to forward
|
|
643
|
+
const replyToId = toMessageId(params.replyToMessageId);
|
|
644
|
+
if (replyToId) {
|
|
645
|
+
return replyToId;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const normalizedCurrentBody = normalizeComparableMessageText(params.currentBody);
|
|
649
|
+
const previousHistoryEntry = [...(params.historyEntries ?? [])]
|
|
650
|
+
.reverse()
|
|
651
|
+
.find((entry) => {
|
|
652
|
+
const messageId = toMessageId(entry.messageId);
|
|
653
|
+
const normalizedBody = normalizeComparableMessageText(entry.body);
|
|
654
|
+
if (!messageId || !normalizedBody) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (normalizedCurrentBody && normalizedBody === normalizedCurrentBody) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return !isLikelyForwardControlMessage(entry.body);
|
|
663
|
+
});
|
|
664
|
+
if (previousHistoryEntry) {
|
|
665
|
+
return toMessageId(previousHistoryEntry.messageId);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(
|
|
669
|
+
params.currentMessageId,
|
|
670
|
+
params.accountId,
|
|
671
|
+
);
|
|
672
|
+
if (previousInboundMessage) {
|
|
673
|
+
if (
|
|
674
|
+
normalizeComparableMessageText(previousInboundMessage.body) !== normalizedCurrentBody
|
|
675
|
+
&& !isLikelyForwardControlMessage(previousInboundMessage.body)
|
|
676
|
+
) {
|
|
677
|
+
return Number(previousInboundMessage.messageId);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function buildBitrix24NativeForwardAgentHint(params: {
|
|
685
|
+
accountId?: string;
|
|
686
|
+
currentBody: string;
|
|
687
|
+
currentMessageId?: string | number;
|
|
688
|
+
replyToMessageId?: string | number;
|
|
689
|
+
historyEntries?: HistoryEntry[];
|
|
690
|
+
}): string | undefined {
|
|
691
|
+
const targetDialogIds = extractBitrix24DialogTargetsFromText(params.currentBody);
|
|
692
|
+
const sourceMessageId = findPreviousForwardableMessageId(params);
|
|
693
|
+
if (!sourceMessageId) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const formattedTargets = targetDialogIds
|
|
698
|
+
.map((dialogId) => (dialogId.startsWith('chat') ? `${dialogId} (chat)` : `${dialogId} (user)`))
|
|
699
|
+
.join(', ');
|
|
700
|
+
|
|
701
|
+
const lines = [
|
|
702
|
+
'[Bitrix24 native forward instruction]',
|
|
703
|
+
'Use this only if you intentionally want a native Bitrix24 forward instead of repeating visible text.',
|
|
704
|
+
`Previous Bitrix24 message id available for forwarding: ${sourceMessageId}.`,
|
|
705
|
+
'Use action "send" with "forwardMessageIds" set to that source message id.',
|
|
706
|
+
'If the tool requires a non-empty message field for a pure forward, use "↩️".',
|
|
707
|
+
'Do not repeat the source message text or the user instruction text when doing a native forward.',
|
|
708
|
+
];
|
|
709
|
+
|
|
710
|
+
if (formattedTargets) {
|
|
711
|
+
lines.push(`Explicit target dialog ids mentioned in the current message: ${formattedTargets}.`);
|
|
712
|
+
lines.push('If you forward to those explicit targets, send one message-tool call per target dialog id and set "to" to the exact dialog id shown above.');
|
|
713
|
+
lines.push('Do not use CHAT=xxx or USER=xxx format for "to" — use "chat520" or "77" directly.');
|
|
714
|
+
} else {
|
|
715
|
+
lines.push('If you forward in the current dialog, do not set "to" to another dialog id.');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
lines.push('[/Bitrix24 native forward instruction]');
|
|
719
|
+
return lines.join('\n');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function buildBitrix24NativeReplyAgentHint(currentMessageId: string | number | undefined): string | undefined {
|
|
723
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
724
|
+
if (!normalizedMessageId) {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return [
|
|
729
|
+
'[Bitrix24 native reply instruction]',
|
|
730
|
+
`Current Bitrix24 message id: ${normalizedMessageId}.`,
|
|
731
|
+
'If you intentionally want a native Bitrix24 reply to the current inbound message, set "replyToMessageId" to that id.',
|
|
732
|
+
'For text-only payloads you can also prepend [[reply_to_current]] to the reply text.',
|
|
733
|
+
'Do not use a native reply unless you actually want reply threading.',
|
|
734
|
+
'[/Bitrix24 native reply instruction]',
|
|
735
|
+
].join('\n');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function buildBitrix24FileDeliveryAgentHint(): string {
|
|
739
|
+
return [
|
|
740
|
+
'[Bitrix24 file delivery instruction]',
|
|
741
|
+
'When you need to deliver a real file or document to the user, use structured reply payload field "mediaUrl" or "mediaUrls".',
|
|
742
|
+
'Set "mediaUrl" to the local path of the generated file in the OpenClaw workspace or managed media directory.',
|
|
743
|
+
'Use "text" only for an optional short caption.',
|
|
744
|
+
'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
|
+
'[/Bitrix24 file delivery instruction]',
|
|
746
|
+
].join('\n');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function readExplicitForwardMessageIds(rawParams: Record<string, unknown>): number[] {
|
|
750
|
+
return parseActionMessageIds(
|
|
751
|
+
rawParams.forwardMessageIds
|
|
752
|
+
?? rawParams.forwardIds,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function resolveActionForwardMessageIds(params: {
|
|
757
|
+
accountId?: string;
|
|
758
|
+
rawParams: Record<string, unknown>;
|
|
759
|
+
toolContext?: { currentMessageId?: string | number };
|
|
760
|
+
}): number[] {
|
|
761
|
+
const explicitForwardMessages = readExplicitForwardMessageIds(params.rawParams);
|
|
762
|
+
if (explicitForwardMessages.length > 0) {
|
|
763
|
+
return explicitForwardMessages;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const aliasedForwardMessages = parseActionMessageIds(
|
|
767
|
+
params.rawParams.messageIds
|
|
768
|
+
?? params.rawParams.message_ids
|
|
769
|
+
?? params.rawParams.messageId
|
|
770
|
+
?? params.rawParams.message_id
|
|
771
|
+
?? params.rawParams.replyToMessageId
|
|
772
|
+
?? params.rawParams.replyToId,
|
|
773
|
+
);
|
|
774
|
+
if (aliasedForwardMessages.length > 0) {
|
|
775
|
+
return aliasedForwardMessages;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(
|
|
779
|
+
params.toolContext?.currentMessageId,
|
|
780
|
+
params.accountId,
|
|
781
|
+
);
|
|
782
|
+
return previousInboundMessage ? [Number(previousInboundMessage.messageId)] : [];
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const FORWARD_MARKER_RE = /^[\s↩️➡️→⤵️🔄📨]+$/u;
|
|
786
|
+
|
|
787
|
+
function normalizeForwardActionText(text: string | null): string {
|
|
788
|
+
const normalizedText = typeof text === 'string' ? text.trim() : '';
|
|
789
|
+
if (!normalizedText || FORWARD_MARKER_RE.test(normalizedText)) {
|
|
790
|
+
return '';
|
|
791
|
+
}
|
|
792
|
+
return normalizedText;
|
|
793
|
+
}
|
|
794
|
+
|
|
173
795
|
function escapeBbCodeText(value: string): string {
|
|
174
796
|
return value
|
|
175
797
|
.replace(/\[/g, '(')
|
|
@@ -1055,6 +1677,28 @@ let gatewayState: GatewayState | null = null;
|
|
|
1055
1677
|
|
|
1056
1678
|
export function __setGatewayStateForTests(state: GatewayState | null): void {
|
|
1057
1679
|
gatewayState = state;
|
|
1680
|
+
if (state === null) {
|
|
1681
|
+
pendingNativeForwardAcks.clear();
|
|
1682
|
+
pendingNativeReactionAcks.clear();
|
|
1683
|
+
recentInboundMessagesByDialog.clear();
|
|
1684
|
+
inboundMessageContextById.clear();
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
export function __rememberRecentInboundMessageForTests(params: {
|
|
1689
|
+
accountId?: string;
|
|
1690
|
+
dialogId: string;
|
|
1691
|
+
messageId: string | number;
|
|
1692
|
+
body: string;
|
|
1693
|
+
timestamp?: number;
|
|
1694
|
+
}): void {
|
|
1695
|
+
rememberRecentInboundMessage(
|
|
1696
|
+
params.dialogId,
|
|
1697
|
+
params.messageId,
|
|
1698
|
+
params.body,
|
|
1699
|
+
params.timestamp,
|
|
1700
|
+
params.accountId,
|
|
1701
|
+
);
|
|
1058
1702
|
}
|
|
1059
1703
|
|
|
1060
1704
|
// ─── Keyboard layouts ────────────────────────────────────────────────────────
|
|
@@ -1072,30 +1716,441 @@ export function buildWelcomeKeyboard(language?: string): B24Keyboard {
|
|
|
1072
1716
|
];
|
|
1073
1717
|
}
|
|
1074
1718
|
|
|
1075
|
-
/** Default keyboard shown with command responses and welcome messages. */
|
|
1076
|
-
export function buildDefaultCommandKeyboard(language?: string): B24Keyboard {
|
|
1077
|
-
const labels = commandKeyboardLabels(language);
|
|
1719
|
+
/** Default keyboard shown with command responses and welcome messages. */
|
|
1720
|
+
export function buildDefaultCommandKeyboard(language?: string): B24Keyboard {
|
|
1721
|
+
const labels = commandKeyboardLabels(language);
|
|
1722
|
+
|
|
1723
|
+
return [
|
|
1724
|
+
{ TEXT: labels.help, COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
|
|
1725
|
+
{ TEXT: labels.status, COMMAND: 'status', DISPLAY: 'LINE' },
|
|
1726
|
+
{ TEXT: labels.commands, COMMAND: 'commands', DISPLAY: 'LINE' },
|
|
1727
|
+
{ TYPE: 'NEWLINE' },
|
|
1728
|
+
{ TEXT: labels.newSession, COMMAND: 'new', DISPLAY: 'LINE' },
|
|
1729
|
+
{ TEXT: labels.models, COMMAND: 'models', DISPLAY: 'LINE' },
|
|
1730
|
+
];
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
export const DEFAULT_COMMAND_KEYBOARD: B24Keyboard = buildDefaultCommandKeyboard();
|
|
1734
|
+
export const DEFAULT_WELCOME_KEYBOARD: B24Keyboard = buildWelcomeKeyboard();
|
|
1735
|
+
|
|
1736
|
+
// ─── Keyboard / Button conversion ────────────────────────────────────────────
|
|
1737
|
+
|
|
1738
|
+
/** Generic button format used by OpenClaw channelData. */
|
|
1739
|
+
export interface ChannelButton {
|
|
1740
|
+
text: string;
|
|
1741
|
+
callback_data?: string;
|
|
1742
|
+
style?: string;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const BITRIX24_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'delete'] as const;
|
|
1746
|
+
const BITRIX24_DISCOVERY_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'delete'] as const;
|
|
1747
|
+
|
|
1748
|
+
function buildMessageToolButtonsSchema(): Record<string, unknown> {
|
|
1749
|
+
return {
|
|
1750
|
+
type: 'array',
|
|
1751
|
+
description: 'Optional Bitrix24 message buttons as rows of button objects.',
|
|
1752
|
+
items: {
|
|
1753
|
+
type: 'array',
|
|
1754
|
+
items: {
|
|
1755
|
+
type: 'object',
|
|
1756
|
+
additionalProperties: false,
|
|
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'],
|
|
1773
|
+
},
|
|
1774
|
+
},
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
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
|
+
return {
|
|
1785
|
+
description: 'Bitrix24 ATTACH rich layout blocks. Use this for rich cards, not for binary file uploads. For a status-owner-link card, prefer blocks like USER, LINK and GRID. Example: [{"USER":{"NAME":"Alice"}},{"LINK":{"NAME":"Open","LINK":"https://example.com"}},{"GRID":[{"NAME":"Status","VALUE":"In progress","DISPLAY":"LINE"},{"NAME":"Owner","VALUE":"Alice","DISPLAY":"LINE"}]}]. Use mediaUrl/mediaUrls for actual files and media.',
|
|
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
|
+
],
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function buildMessageToolIdListSchema(description: string): Record<string, unknown> {
|
|
1813
|
+
return {
|
|
1814
|
+
anyOf: [
|
|
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
|
+
],
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function resolveConfiguredBitrix24ActionAccounts(
|
|
1835
|
+
cfg: Record<string, unknown>,
|
|
1836
|
+
accountId?: string,
|
|
1837
|
+
): string[] {
|
|
1838
|
+
if (accountId) {
|
|
1839
|
+
const account = resolveAccount(cfg, accountId);
|
|
1840
|
+
return account.enabled && account.configured ? [account.accountId] : [];
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
return listAccountIds(cfg).filter((candidateId) => {
|
|
1844
|
+
const account = resolveAccount(cfg, candidateId);
|
|
1845
|
+
return account.enabled && account.configured;
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function describeBitrix24MessageTool(params: {
|
|
1850
|
+
cfg: Record<string, unknown>;
|
|
1851
|
+
accountId?: string;
|
|
1852
|
+
}): ChannelMessageToolDiscovery {
|
|
1853
|
+
const configuredAccounts = resolveConfiguredBitrix24ActionAccounts(
|
|
1854
|
+
params.cfg,
|
|
1855
|
+
params.accountId,
|
|
1856
|
+
);
|
|
1857
|
+
|
|
1858
|
+
if (configuredAccounts.length === 0) {
|
|
1859
|
+
return {
|
|
1860
|
+
actions: [],
|
|
1861
|
+
capabilities: [],
|
|
1862
|
+
schema: null,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
return {
|
|
1867
|
+
actions: [...BITRIX24_DISCOVERY_ACTION_NAMES],
|
|
1868
|
+
capabilities: ['interactive', 'buttons', 'cards'],
|
|
1869
|
+
schema: [
|
|
1870
|
+
{
|
|
1871
|
+
properties: {
|
|
1872
|
+
buttons: buildMessageToolButtonsSchema(),
|
|
1873
|
+
},
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
properties: {
|
|
1877
|
+
attach: buildMessageToolAttachSchema(),
|
|
1878
|
+
forwardMessageIds: buildMessageToolIdListSchema(
|
|
1879
|
+
'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.',
|
|
1880
|
+
),
|
|
1881
|
+
forwardIds: buildMessageToolIdListSchema(
|
|
1882
|
+
'Alias for forwardMessageIds. Use with action="send".',
|
|
1883
|
+
),
|
|
1884
|
+
replyToId: buildMessageToolIdListSchema(
|
|
1885
|
+
'Bitrix24 message id to reply to natively inside the current dialog.',
|
|
1886
|
+
),
|
|
1887
|
+
replyToMessageId: buildMessageToolIdListSchema(
|
|
1888
|
+
'Alias for replyToId.',
|
|
1889
|
+
),
|
|
1890
|
+
},
|
|
1891
|
+
},
|
|
1892
|
+
],
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function extractBitrix24ToolSend(args: Record<string, unknown>): ExtractedToolSendTarget | null {
|
|
1897
|
+
if ((typeof args.action === 'string' ? args.action.trim() : '') !== 'sendMessage') {
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const to = typeof args.to === 'string' ? normalizeBitrix24DialogTarget(args.to) : '';
|
|
1902
|
+
if (!to) {
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const accountId = typeof args.accountId === 'string'
|
|
1907
|
+
? args.accountId.trim()
|
|
1908
|
+
: undefined;
|
|
1909
|
+
const threadId = typeof args.threadId === 'number'
|
|
1910
|
+
? String(args.threadId)
|
|
1911
|
+
: typeof args.threadId === 'string'
|
|
1912
|
+
? args.threadId.trim()
|
|
1913
|
+
: '';
|
|
1914
|
+
|
|
1915
|
+
return {
|
|
1916
|
+
to,
|
|
1917
|
+
...(accountId ? { accountId } : {}),
|
|
1918
|
+
...(threadId ? { threadId } : {}),
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function readActionTextParam(params: Record<string, unknown>): string | null {
|
|
1923
|
+
const rawText = params.message ?? params.text ?? params.content;
|
|
1924
|
+
return typeof rawText === 'string' ? rawText : null;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
1928
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function isAttachColorToken(value: unknown): value is B24AttachColorToken {
|
|
1932
|
+
return value === 'primary'
|
|
1933
|
+
|| value === 'secondary'
|
|
1934
|
+
|| value === 'alert'
|
|
1935
|
+
|| value === 'base';
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function isAttachBlock(value: unknown): value is B24AttachBlock {
|
|
1939
|
+
return isPlainObject(value) && Object.keys(value).length > 0;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function isBitrix24Attach(value: unknown): value is B24Attach {
|
|
1943
|
+
if (Array.isArray(value)) {
|
|
1944
|
+
return value.length > 0 && value.every(isAttachBlock);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
if (!isPlainObject(value) || !Array.isArray(value.BLOCKS) || value.BLOCKS.length === 0) {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
if (value.COLOR_TOKEN !== undefined && !isAttachColorToken(value.COLOR_TOKEN)) {
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
return value.BLOCKS.every(isAttachBlock);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function parseActionAttach(rawValue: unknown): B24Attach | undefined {
|
|
1959
|
+
if (rawValue == null) {
|
|
1960
|
+
return undefined;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
let parsed = rawValue;
|
|
1964
|
+
if (typeof rawValue === 'string') {
|
|
1965
|
+
const trimmed = rawValue.trim();
|
|
1966
|
+
if (!trimmed) {
|
|
1967
|
+
return undefined;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
try {
|
|
1971
|
+
parsed = JSON.parse(trimmed);
|
|
1972
|
+
} catch {
|
|
1973
|
+
return undefined;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
return isBitrix24Attach(parsed) ? parsed : undefined;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
function hasMeaningfulAttachInput(rawValue: unknown): boolean {
|
|
1981
|
+
if (rawValue == null) {
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
if (typeof rawValue === 'string') {
|
|
1986
|
+
const trimmed = rawValue.trim();
|
|
1987
|
+
if (!trimmed) {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
try {
|
|
1992
|
+
return hasMeaningfulAttachInput(JSON.parse(trimmed));
|
|
1993
|
+
} catch {
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
if (Array.isArray(rawValue)) {
|
|
1999
|
+
return rawValue.length > 0;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if (isPlainObject(rawValue)) {
|
|
2003
|
+
if (Array.isArray(rawValue.BLOCKS)) {
|
|
2004
|
+
return rawValue.BLOCKS.length > 0;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
return Object.keys(rawValue).length > 0;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function readActionTargetParam(
|
|
2014
|
+
params: Record<string, unknown>,
|
|
2015
|
+
fallbackTarget?: unknown,
|
|
2016
|
+
): string {
|
|
2017
|
+
const rawTarget = params.to ?? params.target ?? fallbackTarget;
|
|
2018
|
+
return typeof rawTarget === 'string' ? normalizeBitrix24DialogTarget(rawTarget) : '';
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function parseActionMessageIds(rawValue: unknown): number[] {
|
|
2022
|
+
const ids: number[] = [];
|
|
2023
|
+
const seen = new Set<number>();
|
|
2024
|
+
|
|
2025
|
+
const append = (value: unknown) => {
|
|
2026
|
+
if (Array.isArray(value)) {
|
|
2027
|
+
value.forEach(append);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
if (typeof value === 'number') {
|
|
2032
|
+
const normalizedId = Math.trunc(value);
|
|
2033
|
+
if (Number.isFinite(normalizedId) && normalizedId > 0 && !seen.has(normalizedId)) {
|
|
2034
|
+
seen.add(normalizedId);
|
|
2035
|
+
ids.push(normalizedId);
|
|
2036
|
+
}
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
if (typeof value !== 'string') {
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const trimmed = value.trim();
|
|
2045
|
+
if (!trimmed) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if (trimmed.startsWith('[')) {
|
|
2050
|
+
try {
|
|
2051
|
+
append(JSON.parse(trimmed));
|
|
2052
|
+
return;
|
|
2053
|
+
} catch {
|
|
2054
|
+
// Fall through to scalar / CSV parsing.
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
trimmed
|
|
2059
|
+
.split(/[,\s]+/)
|
|
2060
|
+
.forEach((part) => {
|
|
2061
|
+
if (!part) {
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const normalizedId = Number(part);
|
|
2065
|
+
if (Number.isFinite(normalizedId) && normalizedId > 0 && !seen.has(normalizedId)) {
|
|
2066
|
+
seen.add(normalizedId);
|
|
2067
|
+
ids.push(normalizedId);
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
append(rawValue);
|
|
2073
|
+
return ids;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function parseActionKeyboard(rawButtons: unknown): B24Keyboard | undefined {
|
|
2077
|
+
if (rawButtons == null) {
|
|
2078
|
+
return undefined;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
try {
|
|
2082
|
+
const parsed = typeof rawButtons === 'string' ? JSON.parse(rawButtons) : rawButtons;
|
|
2083
|
+
if (Array.isArray(parsed)) {
|
|
2084
|
+
const keyboard = convertButtonsToKeyboard(parsed as ChannelButton[][] | ChannelButton[]);
|
|
2085
|
+
return keyboard.length > 0 ? keyboard : undefined;
|
|
2086
|
+
}
|
|
2087
|
+
} catch {
|
|
2088
|
+
// Ignore invalid buttons payload and send without keyboard.
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
return undefined;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function collectActionMediaUrls(params: Record<string, unknown>): string[] {
|
|
2095
|
+
const mediaUrls: string[] = [];
|
|
2096
|
+
const seen = new Set<string>();
|
|
2097
|
+
|
|
2098
|
+
const append = (value: unknown) => {
|
|
2099
|
+
if (Array.isArray(value)) {
|
|
2100
|
+
value.forEach(append);
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
if (typeof value !== 'string') {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const trimmed = value.trim();
|
|
2109
|
+
if (!trimmed) {
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
if (trimmed.startsWith('[')) {
|
|
2114
|
+
try {
|
|
2115
|
+
append(JSON.parse(trimmed));
|
|
2116
|
+
return;
|
|
2117
|
+
} catch {
|
|
2118
|
+
// Fall through to scalar handling.
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (!seen.has(trimmed)) {
|
|
2123
|
+
seen.add(trimmed);
|
|
2124
|
+
mediaUrls.push(trimmed);
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
1078
2127
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
{ TEXT: labels.models, COMMAND: 'models', DISPLAY: 'LINE' },
|
|
1086
|
-
];
|
|
2128
|
+
append(params.mediaUrl);
|
|
2129
|
+
append(params.mediaUrls);
|
|
2130
|
+
append(params.filePath);
|
|
2131
|
+
append(params.filePaths);
|
|
2132
|
+
|
|
2133
|
+
return mediaUrls;
|
|
1087
2134
|
}
|
|
1088
2135
|
|
|
1089
|
-
|
|
1090
|
-
|
|
2136
|
+
function buildActionMessageText(params: {
|
|
2137
|
+
text: string | null;
|
|
2138
|
+
keyboard?: B24Keyboard;
|
|
2139
|
+
attach?: B24Attach;
|
|
2140
|
+
}): string | null {
|
|
2141
|
+
const normalizedText = typeof params.text === 'string'
|
|
2142
|
+
? markdownToBbCode(params.text.trim())
|
|
2143
|
+
: '';
|
|
2144
|
+
|
|
2145
|
+
if (normalizedText) {
|
|
2146
|
+
return normalizedText;
|
|
2147
|
+
}
|
|
1091
2148
|
|
|
1092
|
-
|
|
2149
|
+
if (params.attach) {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
1093
2152
|
|
|
1094
|
-
|
|
1095
|
-
export interface ChannelButton {
|
|
1096
|
-
text: string;
|
|
1097
|
-
callback_data?: string;
|
|
1098
|
-
style?: string;
|
|
2153
|
+
return params.keyboard ? ' ' : null;
|
|
1099
2154
|
}
|
|
1100
2155
|
|
|
1101
2156
|
function parseRegisteredCommandTrigger(callbackData: string): { command: string; commandParams?: string } | undefined {
|
|
@@ -1188,6 +2243,16 @@ export function extractKeyboardFromPayload(
|
|
|
1188
2243
|
return undefined;
|
|
1189
2244
|
}
|
|
1190
2245
|
|
|
2246
|
+
export function extractAttachFromPayload(
|
|
2247
|
+
payload: { channelData?: Record<string, unknown> },
|
|
2248
|
+
): B24Attach | undefined {
|
|
2249
|
+
const cd = payload.channelData;
|
|
2250
|
+
if (!cd) return undefined;
|
|
2251
|
+
|
|
2252
|
+
const b24Data = cd.bitrix24 as { attach?: unknown; attachments?: unknown } | undefined;
|
|
2253
|
+
return parseActionAttach(b24Data?.attach ?? b24Data?.attachments);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
1191
2256
|
/**
|
|
1192
2257
|
* Extract inline button JSON embedded in message text by the agent.
|
|
1193
2258
|
*
|
|
@@ -1221,6 +2286,14 @@ export function extractInlineButtonsFromText(
|
|
|
1221
2286
|
}
|
|
1222
2287
|
}
|
|
1223
2288
|
|
|
2289
|
+
function cleanupTextAfterInlineMediaExtraction(text: string): string {
|
|
2290
|
+
return text
|
|
2291
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
2292
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
2293
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
2294
|
+
.trim();
|
|
2295
|
+
}
|
|
2296
|
+
|
|
1224
2297
|
function normalizeCommandReplyPayload(params: {
|
|
1225
2298
|
commandName: string;
|
|
1226
2299
|
commandParams: string;
|
|
@@ -1578,11 +2651,12 @@ function resolveOutboundSendCtx(params: {
|
|
|
1578
2651
|
accountId?: string;
|
|
1579
2652
|
}): SendContext | null {
|
|
1580
2653
|
const { config } = resolveAccount(params.cfg, params.accountId);
|
|
1581
|
-
|
|
2654
|
+
const dialogId = normalizeBitrix24DialogTarget(params.to);
|
|
2655
|
+
if (!config.webhookUrl || !gatewayState || !dialogId) return null;
|
|
1582
2656
|
return {
|
|
1583
2657
|
webhookUrl: config.webhookUrl,
|
|
1584
2658
|
bot: gatewayState.bot,
|
|
1585
|
-
dialogId
|
|
2659
|
+
dialogId,
|
|
1586
2660
|
};
|
|
1587
2661
|
}
|
|
1588
2662
|
|
|
@@ -1666,13 +2740,10 @@ export const bitrix24Plugin = {
|
|
|
1666
2740
|
},
|
|
1667
2741
|
|
|
1668
2742
|
messaging: {
|
|
1669
|
-
normalizeTarget: (raw: string) => raw
|
|
2743
|
+
normalizeTarget: (raw: string) => normalizeBitrix24DialogTarget(raw),
|
|
1670
2744
|
targetResolver: {
|
|
1671
|
-
hint: 'Use a numeric
|
|
1672
|
-
looksLikeId: (raw: string, _normalized: string) =>
|
|
1673
|
-
const stripped = raw.trim().replace(CHANNEL_PREFIX_RE, '');
|
|
1674
|
-
return /^\d+$/.test(stripped);
|
|
1675
|
-
},
|
|
2745
|
+
hint: 'Use a numeric dialog ID like "1", a group dialog like "chat42", or Bitrix mentions like "[USER=1]John[/USER]" / "[CHAT=42]Team[/CHAT]".',
|
|
2746
|
+
looksLikeId: (raw: string, _normalized: string) => looksLikeBitrix24DialogTarget(raw),
|
|
1676
2747
|
},
|
|
1677
2748
|
},
|
|
1678
2749
|
|
|
@@ -1787,25 +2858,46 @@ export const bitrix24Plugin = {
|
|
|
1787
2858
|
const sendCtx = resolveOutboundSendCtx(ctx);
|
|
1788
2859
|
if (!sendCtx || !gatewayState) throw new Error('Bitrix24 gateway not started');
|
|
1789
2860
|
|
|
2861
|
+
const text = ctx.text;
|
|
2862
|
+
|
|
1790
2863
|
const keyboard = ctx.payload?.channelData
|
|
1791
2864
|
? extractKeyboardFromPayload({ channelData: ctx.payload.channelData })
|
|
1792
2865
|
: undefined;
|
|
2866
|
+
const attach = ctx.payload?.channelData
|
|
2867
|
+
? extractAttachFromPayload({ channelData: ctx.payload.channelData })
|
|
2868
|
+
: undefined;
|
|
1793
2869
|
const mediaUrls = collectOutboundMediaUrls({
|
|
1794
2870
|
mediaUrl: ctx.mediaUrl,
|
|
1795
2871
|
payload: ctx.payload as { mediaUrl?: string; mediaUrls?: string[] } | undefined,
|
|
1796
2872
|
});
|
|
1797
2873
|
|
|
1798
2874
|
if (mediaUrls.length > 0) {
|
|
2875
|
+
const initialMessage = !keyboard && !attach ? text || undefined : undefined;
|
|
1799
2876
|
const uploadedMessageId = await uploadOutboundMedia({
|
|
1800
2877
|
mediaService: gatewayState.mediaService,
|
|
1801
2878
|
sendCtx,
|
|
1802
2879
|
mediaUrls,
|
|
2880
|
+
initialMessage,
|
|
1803
2881
|
});
|
|
1804
2882
|
|
|
1805
|
-
if (
|
|
2883
|
+
if ((text && !initialMessage) || keyboard || attach) {
|
|
2884
|
+
if (attach) {
|
|
2885
|
+
const messageId = await gatewayState.api.sendMessage(
|
|
2886
|
+
sendCtx.webhookUrl,
|
|
2887
|
+
sendCtx.bot,
|
|
2888
|
+
sendCtx.dialogId,
|
|
2889
|
+
buildActionMessageText({ text, keyboard, attach }),
|
|
2890
|
+
{
|
|
2891
|
+
...(keyboard ? { keyboard } : {}),
|
|
2892
|
+
attach,
|
|
2893
|
+
},
|
|
2894
|
+
);
|
|
2895
|
+
return { messageId: String(messageId || uploadedMessageId) };
|
|
2896
|
+
}
|
|
2897
|
+
|
|
1806
2898
|
const result = await gatewayState.sendService.sendText(
|
|
1807
2899
|
sendCtx,
|
|
1808
|
-
|
|
2900
|
+
text || '',
|
|
1809
2901
|
keyboard ? { keyboard } : undefined,
|
|
1810
2902
|
);
|
|
1811
2903
|
return { messageId: String(result.messageId ?? uploadedMessageId) };
|
|
@@ -1814,10 +2906,24 @@ export const bitrix24Plugin = {
|
|
|
1814
2906
|
return { messageId: uploadedMessageId };
|
|
1815
2907
|
}
|
|
1816
2908
|
|
|
1817
|
-
if (
|
|
2909
|
+
if (text || keyboard || attach) {
|
|
2910
|
+
if (attach) {
|
|
2911
|
+
const messageId = await gatewayState.api.sendMessage(
|
|
2912
|
+
sendCtx.webhookUrl,
|
|
2913
|
+
sendCtx.bot,
|
|
2914
|
+
sendCtx.dialogId,
|
|
2915
|
+
buildActionMessageText({ text, keyboard, attach }),
|
|
2916
|
+
{
|
|
2917
|
+
...(keyboard ? { keyboard } : {}),
|
|
2918
|
+
attach,
|
|
2919
|
+
},
|
|
2920
|
+
);
|
|
2921
|
+
return { messageId: String(messageId ?? '') };
|
|
2922
|
+
}
|
|
2923
|
+
|
|
1818
2924
|
const result = await gatewayState.sendService.sendText(
|
|
1819
2925
|
sendCtx,
|
|
1820
|
-
|
|
2926
|
+
text || '',
|
|
1821
2927
|
keyboard ? { keyboard } : undefined,
|
|
1822
2928
|
);
|
|
1823
2929
|
return { messageId: String(result.messageId ?? '') };
|
|
@@ -1829,12 +2935,23 @@ export const bitrix24Plugin = {
|
|
|
1829
2935
|
// ─── Actions (agent-driven: reactions, etc.) ────────────────────────────
|
|
1830
2936
|
|
|
1831
2937
|
actions: {
|
|
2938
|
+
describeMessageTool: (params: {
|
|
2939
|
+
cfg: Record<string, unknown>;
|
|
2940
|
+
accountId?: string;
|
|
2941
|
+
}): ChannelMessageToolDiscovery => describeBitrix24MessageTool(params),
|
|
2942
|
+
|
|
2943
|
+
extractToolSend: (params: {
|
|
2944
|
+
args: Record<string, unknown>;
|
|
2945
|
+
}): ExtractedToolSendTarget | null => extractBitrix24ToolSend(params.args),
|
|
2946
|
+
|
|
1832
2947
|
listActions: (_params: { cfg: Record<string, unknown> }): string[] => {
|
|
1833
|
-
return [
|
|
2948
|
+
return [...BITRIX24_DISCOVERY_ACTION_NAMES];
|
|
1834
2949
|
},
|
|
1835
2950
|
|
|
1836
2951
|
supportsAction: (params: { action: string }): boolean => {
|
|
1837
|
-
return
|
|
2952
|
+
return BITRIX24_ACTION_NAMES.includes(
|
|
2953
|
+
params.action as typeof BITRIX24_ACTION_NAMES[number],
|
|
2954
|
+
);
|
|
1838
2955
|
},
|
|
1839
2956
|
|
|
1840
2957
|
handleAction: async (ctx: {
|
|
@@ -1853,35 +2970,270 @@ export const bitrix24Plugin = {
|
|
|
1853
2970
|
|
|
1854
2971
|
// ─── Send with buttons ──────────────────────────────────────────────
|
|
1855
2972
|
if (ctx.action === 'send') {
|
|
1856
|
-
// Only intercept send when buttons are present; otherwise let gateway handle normally
|
|
1857
2973
|
const rawButtons = ctx.params.buttons;
|
|
1858
|
-
|
|
2974
|
+
const rawAttach = ctx.params.attach ?? ctx.params.attachments;
|
|
1859
2975
|
|
|
1860
2976
|
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
1861
2977
|
if (!config.webhookUrl || !gatewayState) return null;
|
|
1862
2978
|
|
|
1863
2979
|
const bot = gatewayState.bot;
|
|
1864
|
-
const to =
|
|
2980
|
+
const to = readActionTargetParam(ctx.params, ctx.to);
|
|
1865
2981
|
if (!to) {
|
|
1866
2982
|
defaultLogger.warn('handleAction send: no "to" in params or ctx, falling back to gateway');
|
|
1867
2983
|
return null;
|
|
1868
2984
|
}
|
|
1869
2985
|
|
|
1870
2986
|
const sendCtx: SendContext = { webhookUrl: config.webhookUrl, bot, dialogId: to };
|
|
1871
|
-
const
|
|
2987
|
+
const messageText = readActionTextParam(ctx.params);
|
|
2988
|
+
const message = typeof messageText === 'string' ? messageText.trim() : '';
|
|
2989
|
+
const keyboard = parseActionKeyboard(rawButtons);
|
|
2990
|
+
const attach = parseActionAttach(rawAttach);
|
|
2991
|
+
const mediaUrls = collectActionMediaUrls(ctx.params);
|
|
2992
|
+
const toolContext = (ctx as Record<string, unknown>).toolContext as
|
|
2993
|
+
| { currentMessageId?: string | number } | undefined;
|
|
2994
|
+
const currentToolMessageId = toolContext?.currentMessageId;
|
|
2995
|
+
const currentInboundContextKey = buildAccountMessageScopeKey(
|
|
2996
|
+
ctx.accountId,
|
|
2997
|
+
currentToolMessageId,
|
|
2998
|
+
);
|
|
2999
|
+
const currentInboundContext = currentInboundContextKey
|
|
3000
|
+
? inboundMessageContextById.get(currentInboundContextKey)
|
|
3001
|
+
: undefined;
|
|
3002
|
+
const forwardAckDialogId = resolvePendingNativeForwardAckDialogId(
|
|
3003
|
+
currentToolMessageId,
|
|
3004
|
+
to,
|
|
3005
|
+
ctx.accountId,
|
|
3006
|
+
);
|
|
3007
|
+
const explicitForwardMessages = readExplicitForwardMessageIds(ctx.params);
|
|
3008
|
+
const referencedReplyMessageId = parseActionMessageIds(
|
|
3009
|
+
ctx.params.replyToMessageId ?? ctx.params.replyToId,
|
|
3010
|
+
)[0];
|
|
3011
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
3012
|
+
return toolResult({ ok: false, reason: 'invalid_attach', hint: 'Bitrix24 attach must use ATTACH rich blocks. Do not pass uploaded files here; use mediaUrl/mediaUrls for file uploads.' });
|
|
3013
|
+
}
|
|
3014
|
+
if (explicitForwardMessages.length > 0 && mediaUrls.length > 0) {
|
|
3015
|
+
return toolResult({
|
|
3016
|
+
ok: false,
|
|
3017
|
+
reason: 'unsupported_payload_combo',
|
|
3018
|
+
hint: 'Bitrix24 native forward cannot be combined with mediaUrl/mediaUrls uploads in one send action. Send the forward separately.',
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
if (!message && !keyboard && !attach && mediaUrls.length === 0 && explicitForwardMessages.length === 0) {
|
|
3022
|
+
return toolResult({
|
|
3023
|
+
ok: false,
|
|
3024
|
+
reason: 'missing_payload',
|
|
3025
|
+
hint: 'Provide message text, buttons, rich attach blocks or mediaUrl/mediaUrls for Bitrix24 send.',
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
1872
3028
|
|
|
1873
|
-
// Parse buttons: may be array or JSON string
|
|
1874
|
-
let buttons: ChannelButton[][] | undefined;
|
|
1875
3029
|
try {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
3030
|
+
if (explicitForwardMessages.length > 0) {
|
|
3031
|
+
const normalizedForwardText = normalizeForwardActionText(messageText);
|
|
3032
|
+
|
|
3033
|
+
if (attach) {
|
|
3034
|
+
const messageId = await gatewayState.api.sendMessage(
|
|
3035
|
+
config.webhookUrl,
|
|
3036
|
+
bot,
|
|
3037
|
+
to,
|
|
3038
|
+
buildActionMessageText({ text: normalizedForwardText, keyboard, attach }),
|
|
3039
|
+
{
|
|
3040
|
+
...(keyboard ? { keyboard } : {}),
|
|
3041
|
+
attach,
|
|
3042
|
+
forwardMessages: explicitForwardMessages,
|
|
3043
|
+
},
|
|
3044
|
+
);
|
|
3045
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
3046
|
+
return toolResult({
|
|
3047
|
+
channel: 'bitrix24',
|
|
3048
|
+
to,
|
|
3049
|
+
via: 'direct',
|
|
3050
|
+
mediaUrl: null,
|
|
3051
|
+
forwarded: true,
|
|
3052
|
+
forwardMessages: explicitForwardMessages,
|
|
3053
|
+
result: { messageId: String(messageId ?? '') },
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
const result = await gatewayState.sendService.sendText(
|
|
3058
|
+
sendCtx,
|
|
3059
|
+
normalizedForwardText || (keyboard ? ' ' : ''),
|
|
3060
|
+
{
|
|
3061
|
+
...(keyboard ? { keyboard } : {}),
|
|
3062
|
+
forwardMessages: explicitForwardMessages,
|
|
3063
|
+
},
|
|
3064
|
+
);
|
|
3065
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
3066
|
+
return toolResult({
|
|
3067
|
+
channel: 'bitrix24',
|
|
3068
|
+
to,
|
|
3069
|
+
via: 'direct',
|
|
3070
|
+
mediaUrl: null,
|
|
3071
|
+
forwarded: true,
|
|
3072
|
+
forwardMessages: explicitForwardMessages,
|
|
3073
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
1881
3076
|
|
|
1882
|
-
|
|
3077
|
+
if (referencedReplyMessageId && !attach && mediaUrls.length === 0) {
|
|
3078
|
+
let referencedMessageText = '';
|
|
3079
|
+
|
|
3080
|
+
try {
|
|
3081
|
+
const referencedMessage = await gatewayState.api.getMessage(
|
|
3082
|
+
config.webhookUrl,
|
|
3083
|
+
bot,
|
|
3084
|
+
referencedReplyMessageId,
|
|
3085
|
+
);
|
|
3086
|
+
referencedMessageText = resolveFetchedMessageBody(referencedMessage.message?.text ?? '');
|
|
3087
|
+
} catch (err) {
|
|
3088
|
+
defaultLogger.debug('Failed to hydrate referenced Bitrix24 message for send action', err);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
const normalizedReferencedMessage = normalizeComparableMessageText(referencedMessageText);
|
|
3092
|
+
const normalizedMessage = normalizeComparableMessageText(message);
|
|
3093
|
+
|
|
3094
|
+
if (normalizedReferencedMessage && normalizedReferencedMessage === normalizedMessage) {
|
|
3095
|
+
const result = await gatewayState.sendService.sendText(
|
|
3096
|
+
sendCtx,
|
|
3097
|
+
'',
|
|
3098
|
+
{ forwardMessages: [referencedReplyMessageId] },
|
|
3099
|
+
);
|
|
3100
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
3101
|
+
return toolResult({
|
|
3102
|
+
channel: 'bitrix24',
|
|
3103
|
+
to,
|
|
3104
|
+
via: 'direct',
|
|
3105
|
+
mediaUrl: null,
|
|
3106
|
+
forwarded: true,
|
|
3107
|
+
forwardMessages: [referencedReplyMessageId],
|
|
3108
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
const result = await gatewayState.sendService.sendText(
|
|
3113
|
+
sendCtx,
|
|
3114
|
+
message || ' ',
|
|
3115
|
+
{
|
|
3116
|
+
...(keyboard ? { keyboard } : {}),
|
|
3117
|
+
replyToMessageId: referencedReplyMessageId,
|
|
3118
|
+
},
|
|
3119
|
+
);
|
|
3120
|
+
return toolResult({
|
|
3121
|
+
channel: 'bitrix24',
|
|
3122
|
+
to,
|
|
3123
|
+
via: 'direct',
|
|
3124
|
+
mediaUrl: null,
|
|
3125
|
+
replied: true,
|
|
3126
|
+
replyToMessageId: referencedReplyMessageId,
|
|
3127
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(
|
|
3132
|
+
currentToolMessageId,
|
|
3133
|
+
ctx.accountId,
|
|
3134
|
+
);
|
|
3135
|
+
|
|
3136
|
+
if (
|
|
3137
|
+
!referencedReplyMessageId
|
|
3138
|
+
&& !attach
|
|
3139
|
+
&& mediaUrls.length === 0
|
|
3140
|
+
&& !keyboard
|
|
3141
|
+
&& previousInboundMessage
|
|
3142
|
+
&& currentInboundContext
|
|
3143
|
+
&& normalizeComparableMessageText(message)
|
|
3144
|
+
&& normalizeComparableMessageText(message) === normalizeComparableMessageText(previousInboundMessage.body)
|
|
3145
|
+
&& normalizeComparableMessageText(currentInboundContext.body) !== normalizeComparableMessageText(message)
|
|
3146
|
+
) {
|
|
3147
|
+
const result = await gatewayState.sendService.sendText(
|
|
3148
|
+
sendCtx,
|
|
3149
|
+
'',
|
|
3150
|
+
{ forwardMessages: [Number(previousInboundMessage.messageId)] },
|
|
3151
|
+
);
|
|
3152
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
3153
|
+
return toolResult({
|
|
3154
|
+
channel: 'bitrix24',
|
|
3155
|
+
to,
|
|
3156
|
+
via: 'direct',
|
|
3157
|
+
mediaUrl: null,
|
|
3158
|
+
forwarded: true,
|
|
3159
|
+
forwardMessages: [Number(previousInboundMessage.messageId)],
|
|
3160
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
if (mediaUrls.length > 0) {
|
|
3165
|
+
const initialMessage = !keyboard && !attach ? message || undefined : undefined;
|
|
3166
|
+
const uploadedMessageId = await uploadOutboundMedia({
|
|
3167
|
+
mediaService: gatewayState.mediaService,
|
|
3168
|
+
sendCtx,
|
|
3169
|
+
mediaUrls,
|
|
3170
|
+
initialMessage,
|
|
3171
|
+
});
|
|
3172
|
+
|
|
3173
|
+
if ((message && !initialMessage) || keyboard || attach) {
|
|
3174
|
+
if (attach) {
|
|
3175
|
+
const messageId = await gatewayState.api.sendMessage(
|
|
3176
|
+
config.webhookUrl,
|
|
3177
|
+
bot,
|
|
3178
|
+
to,
|
|
3179
|
+
buildActionMessageText({ text: message, keyboard, attach }),
|
|
3180
|
+
{
|
|
3181
|
+
...(keyboard ? { keyboard } : {}),
|
|
3182
|
+
attach,
|
|
3183
|
+
},
|
|
3184
|
+
);
|
|
3185
|
+
return toolResult({
|
|
3186
|
+
channel: 'bitrix24',
|
|
3187
|
+
to,
|
|
3188
|
+
via: 'direct',
|
|
3189
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
3190
|
+
result: { messageId: String(messageId ?? uploadedMessageId) },
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
const result = await gatewayState.sendService.sendText(
|
|
3195
|
+
sendCtx,
|
|
3196
|
+
message || '',
|
|
3197
|
+
keyboard ? { keyboard } : undefined,
|
|
3198
|
+
);
|
|
3199
|
+
return toolResult({
|
|
3200
|
+
channel: 'bitrix24',
|
|
3201
|
+
to,
|
|
3202
|
+
via: 'direct',
|
|
3203
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
3204
|
+
result: { messageId: String(result.messageId ?? uploadedMessageId) },
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
return toolResult({
|
|
3209
|
+
channel: 'bitrix24',
|
|
3210
|
+
to,
|
|
3211
|
+
via: 'direct',
|
|
3212
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
3213
|
+
result: { messageId: uploadedMessageId },
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
if (attach) {
|
|
3218
|
+
const messageId = await gatewayState.api.sendMessage(
|
|
3219
|
+
config.webhookUrl,
|
|
3220
|
+
bot,
|
|
3221
|
+
to,
|
|
3222
|
+
buildActionMessageText({ text: message, keyboard, attach }),
|
|
3223
|
+
{
|
|
3224
|
+
...(keyboard ? { keyboard } : {}),
|
|
3225
|
+
attach,
|
|
3226
|
+
},
|
|
3227
|
+
);
|
|
3228
|
+
return toolResult({
|
|
3229
|
+
channel: 'bitrix24',
|
|
3230
|
+
to,
|
|
3231
|
+
via: 'direct',
|
|
3232
|
+
mediaUrl: null,
|
|
3233
|
+
result: { messageId: String(messageId ?? '') },
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
1883
3236
|
|
|
1884
|
-
try {
|
|
1885
3237
|
const result = await gatewayState.sendService.sendText(
|
|
1886
3238
|
sendCtx, message || ' ', keyboard ? { keyboard } : undefined,
|
|
1887
3239
|
);
|
|
@@ -1898,6 +3250,192 @@ export const bitrix24Plugin = {
|
|
|
1898
3250
|
}
|
|
1899
3251
|
}
|
|
1900
3252
|
|
|
3253
|
+
if (ctx.action === 'reply') {
|
|
3254
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
3255
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
3256
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
const params = ctx.params;
|
|
3260
|
+
const bot = gatewayState.bot;
|
|
3261
|
+
const to = readActionTargetParam(params, ctx.to);
|
|
3262
|
+
if (!to) {
|
|
3263
|
+
return toolResult({ ok: false, reason: 'missing_target', hint: 'Bitrix24 reply requires a target dialog id in "to" or "target". Do not retry.' });
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
const toolContext = (ctx as Record<string, unknown>).toolContext as
|
|
3267
|
+
| { currentMessageId?: string | number } | undefined;
|
|
3268
|
+
const replyToMessageId = parseActionMessageIds(
|
|
3269
|
+
params.replyToMessageId
|
|
3270
|
+
?? params.replyToId
|
|
3271
|
+
?? params.messageId
|
|
3272
|
+
?? params.message_id
|
|
3273
|
+
?? toolContext?.currentMessageId,
|
|
3274
|
+
)[0];
|
|
3275
|
+
|
|
3276
|
+
if (!replyToMessageId) {
|
|
3277
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Bitrix24 reply requires a valid target messageId. Do not retry.' });
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
const text = readActionTextParam(params);
|
|
3281
|
+
const normalizedText = typeof text === 'string' ? text.trim() : '';
|
|
3282
|
+
const keyboard = parseActionKeyboard(params.buttons);
|
|
3283
|
+
const rawAttach = params.attach ?? params.attachments;
|
|
3284
|
+
const attach = parseActionAttach(rawAttach);
|
|
3285
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
3286
|
+
return toolResult({ ok: false, reason: 'invalid_attach', hint: 'Bitrix24 attach must use ATTACH rich blocks. Do not pass uploaded files here; use mediaUrl/mediaUrls for file uploads.' });
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
if (!normalizedText && !keyboard && !attach) {
|
|
3290
|
+
return toolResult({ ok: false, reason: 'missing_payload', hint: 'Provide reply text, buttons or rich attach blocks for Bitrix24 reply.' });
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
try {
|
|
3294
|
+
if (attach) {
|
|
3295
|
+
const messageId = await gatewayState.api.sendMessage(
|
|
3296
|
+
config.webhookUrl,
|
|
3297
|
+
bot,
|
|
3298
|
+
to,
|
|
3299
|
+
buildActionMessageText({ text: normalizedText, keyboard, attach }),
|
|
3300
|
+
{
|
|
3301
|
+
...(keyboard ? { keyboard } : {}),
|
|
3302
|
+
attach,
|
|
3303
|
+
replyToMessageId,
|
|
3304
|
+
},
|
|
3305
|
+
);
|
|
3306
|
+
return toolResult({
|
|
3307
|
+
ok: true,
|
|
3308
|
+
replied: true,
|
|
3309
|
+
to,
|
|
3310
|
+
replyToMessageId,
|
|
3311
|
+
messageId: String(messageId ?? ''),
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
const result = await gatewayState.sendService.sendText(
|
|
3316
|
+
{ webhookUrl: config.webhookUrl, bot, dialogId: to },
|
|
3317
|
+
normalizedText || ' ',
|
|
3318
|
+
{
|
|
3319
|
+
...(keyboard ? { keyboard } : {}),
|
|
3320
|
+
replyToMessageId,
|
|
3321
|
+
},
|
|
3322
|
+
);
|
|
3323
|
+
return toolResult({
|
|
3324
|
+
ok: true,
|
|
3325
|
+
replied: true,
|
|
3326
|
+
to,
|
|
3327
|
+
replyToMessageId,
|
|
3328
|
+
messageId: String(result.messageId ?? ''),
|
|
3329
|
+
});
|
|
3330
|
+
} catch (err) {
|
|
3331
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3332
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to send Bitrix24 reply: ${errMsg}. Do not retry.` });
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// Note: "forward" action is not supported by OpenClaw runtime (not in
|
|
3337
|
+
// MESSAGE_ACTION_TARGET_MODE), so the runtime blocks target before it
|
|
3338
|
+
// reaches handleAction. All forwarding goes through action "send" with
|
|
3339
|
+
// forwardMessageIds parameter instead.
|
|
3340
|
+
|
|
3341
|
+
if (ctx.action === 'edit') {
|
|
3342
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
3343
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
3344
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
const bot = gatewayState.bot;
|
|
3348
|
+
const api = gatewayState.api;
|
|
3349
|
+
const params = ctx.params;
|
|
3350
|
+
const toolContext = (ctx as Record<string, unknown>).toolContext as
|
|
3351
|
+
| { currentMessageId?: string | number } | undefined;
|
|
3352
|
+
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
3353
|
+
const messageId = Number(rawMessageId);
|
|
3354
|
+
|
|
3355
|
+
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
3356
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 edits. Do not retry.' });
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
let keyboard: B24Keyboard | 'N' | undefined;
|
|
3360
|
+
if (params.buttons === 'N') {
|
|
3361
|
+
keyboard = 'N';
|
|
3362
|
+
} else if (params.buttons != null) {
|
|
3363
|
+
try {
|
|
3364
|
+
const parsed = typeof params.buttons === 'string'
|
|
3365
|
+
? JSON.parse(params.buttons)
|
|
3366
|
+
: params.buttons;
|
|
3367
|
+
if (Array.isArray(parsed)) {
|
|
3368
|
+
keyboard = convertButtonsToKeyboard(parsed as ChannelButton[][] | ChannelButton[]);
|
|
3369
|
+
}
|
|
3370
|
+
} catch {
|
|
3371
|
+
// Ignore invalid buttons payload and update text only.
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
const message = readActionTextParam(params);
|
|
3376
|
+
const rawAttach = params.attach ?? params.attachments;
|
|
3377
|
+
const attach = parseActionAttach(rawAttach);
|
|
3378
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
3379
|
+
return toolResult({ ok: false, reason: 'invalid_attach', hint: 'Bitrix24 attach must use ATTACH rich blocks. Do not pass uploaded files here; use mediaUrl/mediaUrls for file uploads.' });
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
if (message == null && !keyboard && !attach) {
|
|
3383
|
+
return toolResult({ ok: false, reason: 'missing_payload', hint: 'Provide message text, buttons or rich attach blocks for Bitrix24 edits.' });
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
try {
|
|
3387
|
+
await api.updateMessage(
|
|
3388
|
+
config.webhookUrl,
|
|
3389
|
+
bot,
|
|
3390
|
+
messageId,
|
|
3391
|
+
message,
|
|
3392
|
+
{
|
|
3393
|
+
...(keyboard ? { keyboard } : {}),
|
|
3394
|
+
...(attach ? { attach } : {}),
|
|
3395
|
+
},
|
|
3396
|
+
);
|
|
3397
|
+
return toolResult({ ok: true, edited: true, messageId });
|
|
3398
|
+
} catch (err) {
|
|
3399
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3400
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to edit message: ${errMsg}. Do not retry.` });
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
if (ctx.action === 'delete') {
|
|
3405
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
3406
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
3407
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
const bot = gatewayState.bot;
|
|
3411
|
+
const api = gatewayState.api;
|
|
3412
|
+
const params = ctx.params;
|
|
3413
|
+
const toolContext = (ctx as Record<string, unknown>).toolContext as
|
|
3414
|
+
| { currentMessageId?: string | number } | undefined;
|
|
3415
|
+
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
3416
|
+
const messageId = Number(rawMessageId);
|
|
3417
|
+
|
|
3418
|
+
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
3419
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 deletes. Do not retry.' });
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
const complete = params.complete == null
|
|
3423
|
+
? undefined
|
|
3424
|
+
: (
|
|
3425
|
+
params.complete === true
|
|
3426
|
+
|| params.complete === 'true'
|
|
3427
|
+
|| params.complete === 'Y'
|
|
3428
|
+
);
|
|
3429
|
+
|
|
3430
|
+
try {
|
|
3431
|
+
await api.deleteMessage(config.webhookUrl, bot, messageId, complete);
|
|
3432
|
+
return toolResult({ ok: true, deleted: true, messageId });
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3435
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to delete message: ${errMsg}. Do not retry.` });
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
|
|
1901
3439
|
// ─── React ────────────────────────────────────────────────────────────
|
|
1902
3440
|
if (ctx.action !== 'react') return null;
|
|
1903
3441
|
|
|
@@ -1955,11 +3493,13 @@ export const bitrix24Plugin = {
|
|
|
1955
3493
|
|
|
1956
3494
|
try {
|
|
1957
3495
|
await api.addReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
3496
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
1958
3497
|
return toolResult({ ok: true, added: emoji });
|
|
1959
3498
|
} catch (err) {
|
|
1960
3499
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1961
3500
|
const isAlreadySet = errMsg.includes('REACTION_ALREADY_SET');
|
|
1962
3501
|
if (isAlreadySet) {
|
|
3502
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
1963
3503
|
return toolResult({ ok: true, added: emoji, warning: 'Reaction already set.' });
|
|
1964
3504
|
}
|
|
1965
3505
|
return toolResult({ ok: false, reason: 'error', emoji, hint: `Reaction failed: ${errMsg}. Do not retry.` });
|
|
@@ -2327,9 +3867,22 @@ export const bitrix24Plugin = {
|
|
|
2327
3867
|
query: bodyWithReply,
|
|
2328
3868
|
historyCache,
|
|
2329
3869
|
});
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
|
|
3870
|
+
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
3871
|
+
const nativeReplyAgentHint = buildBitrix24NativeReplyAgentHint(msgCtx.messageId);
|
|
3872
|
+
const nativeForwardAgentHint = buildBitrix24NativeForwardAgentHint({
|
|
3873
|
+
accountId: ctx.accountId,
|
|
3874
|
+
currentBody: body,
|
|
3875
|
+
currentMessageId: msgCtx.messageId,
|
|
3876
|
+
replyToMessageId: msgCtx.replyToMessageId,
|
|
3877
|
+
historyEntries: previousEntries,
|
|
3878
|
+
});
|
|
3879
|
+
const bodyForAgent = [
|
|
3880
|
+
fileDeliveryAgentHint,
|
|
3881
|
+
nativeReplyAgentHint,
|
|
3882
|
+
nativeForwardAgentHint,
|
|
3883
|
+
crossChatContext,
|
|
3884
|
+
bodyWithReply,
|
|
3885
|
+
].filter(Boolean).join('\n\n');
|
|
2333
3886
|
const combinedBody = msgCtx.isGroup
|
|
2334
3887
|
? buildHistoryContext({
|
|
2335
3888
|
entries: previousEntries,
|
|
@@ -2338,6 +3891,13 @@ export const bitrix24Plugin = {
|
|
|
2338
3891
|
: bodyForAgent;
|
|
2339
3892
|
|
|
2340
3893
|
recordHistory(body);
|
|
3894
|
+
rememberRecentInboundMessage(
|
|
3895
|
+
conversation.dialogId,
|
|
3896
|
+
msgCtx.messageId,
|
|
3897
|
+
body,
|
|
3898
|
+
msgCtx.timestamp ?? Date.now(),
|
|
3899
|
+
ctx.accountId,
|
|
3900
|
+
);
|
|
2341
3901
|
|
|
2342
3902
|
// Resolve which agent handles this conversation
|
|
2343
3903
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
@@ -2402,7 +3962,31 @@ export const bitrix24Plugin = {
|
|
|
2402
3962
|
});
|
|
2403
3963
|
}
|
|
2404
3964
|
if (payload.text) {
|
|
2405
|
-
|
|
3965
|
+
if (consumePendingNativeForwardAck(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
3966
|
+
replyDelivered = true;
|
|
3967
|
+
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 forward', {
|
|
3968
|
+
senderId: msgCtx.senderId,
|
|
3969
|
+
chatId: msgCtx.chatId,
|
|
3970
|
+
messageId: msgCtx.messageId,
|
|
3971
|
+
});
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
const replyDirective = extractReplyDirective({
|
|
3976
|
+
text: payload.text,
|
|
3977
|
+
currentMessageId: msgCtx.messageId,
|
|
3978
|
+
explicitReplyToId: (payload as Record<string, unknown>).replyToId,
|
|
3979
|
+
});
|
|
3980
|
+
let text = replyDirective.cleanText;
|
|
3981
|
+
if (consumePendingNativeReactionAck(msgCtx.messageId, text, ctx.accountId)) {
|
|
3982
|
+
replyDelivered = true;
|
|
3983
|
+
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 reaction', {
|
|
3984
|
+
senderId: msgCtx.senderId,
|
|
3985
|
+
chatId: msgCtx.chatId,
|
|
3986
|
+
messageId: msgCtx.messageId,
|
|
3987
|
+
});
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
2406
3990
|
let keyboard = extractKeyboardFromPayload(payload);
|
|
2407
3991
|
|
|
2408
3992
|
// Fallback: agent may embed button JSON in text as [[{...}]]
|
|
@@ -2414,8 +3998,68 @@ export const bitrix24Plugin = {
|
|
|
2414
3998
|
}
|
|
2415
3999
|
}
|
|
2416
4000
|
|
|
4001
|
+
const nativeForwardMessageId = findNativeForwardTarget({
|
|
4002
|
+
accountId: ctx.accountId,
|
|
4003
|
+
requestText: body,
|
|
4004
|
+
deliveredText: text,
|
|
4005
|
+
historyEntries: previousEntries,
|
|
4006
|
+
currentMessageId: msgCtx.messageId,
|
|
4007
|
+
}) ?? findImplicitForwardTargetFromRecentInbound({
|
|
4008
|
+
accountId: ctx.accountId,
|
|
4009
|
+
requestText: body,
|
|
4010
|
+
deliveredText: text,
|
|
4011
|
+
currentMessageId: msgCtx.messageId,
|
|
4012
|
+
historyEntries: previousEntries,
|
|
4013
|
+
});
|
|
4014
|
+
const nativeForwardTargets = nativeForwardMessageId
|
|
4015
|
+
? extractBitrix24DialogTargetsFromText(body)
|
|
4016
|
+
: [];
|
|
4017
|
+
const nativeReplyToMessageId = nativeForwardMessageId
|
|
4018
|
+
? undefined
|
|
4019
|
+
: replyDirective.replyToMessageId;
|
|
4020
|
+
const sendOptions = {
|
|
4021
|
+
...(keyboard ? { keyboard } : {}),
|
|
4022
|
+
...(nativeReplyToMessageId ? { replyToMessageId: nativeReplyToMessageId } : {}),
|
|
4023
|
+
...(nativeForwardMessageId ? { forwardMessages: [nativeForwardMessageId] } : {}),
|
|
4024
|
+
};
|
|
4025
|
+
|
|
2417
4026
|
replyDelivered = true;
|
|
2418
|
-
|
|
4027
|
+
if (nativeForwardMessageId && nativeForwardTargets.length > 0) {
|
|
4028
|
+
const forwardResults = await Promise.allSettled(
|
|
4029
|
+
nativeForwardTargets.map((dialogId) => sendService.sendText(
|
|
4030
|
+
{ ...sendCtx, dialogId },
|
|
4031
|
+
'',
|
|
4032
|
+
{ forwardMessages: [nativeForwardMessageId] },
|
|
4033
|
+
)),
|
|
4034
|
+
);
|
|
4035
|
+
const firstFailure = forwardResults.find(
|
|
4036
|
+
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
|
4037
|
+
);
|
|
4038
|
+
const hasSuccess = forwardResults.some((result) => result.status === 'fulfilled');
|
|
4039
|
+
|
|
4040
|
+
if (firstFailure && !hasSuccess) {
|
|
4041
|
+
throw firstFailure.reason;
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
if (firstFailure) {
|
|
4045
|
+
logger.warn('Failed to deliver Bitrix24 native forward to one or more explicit targets', {
|
|
4046
|
+
senderId: msgCtx.senderId,
|
|
4047
|
+
chatId: msgCtx.chatId,
|
|
4048
|
+
messageId: msgCtx.messageId,
|
|
4049
|
+
targets: nativeForwardTargets,
|
|
4050
|
+
error: firstFailure.reason instanceof Error
|
|
4051
|
+
? firstFailure.reason.message
|
|
4052
|
+
: String(firstFailure.reason),
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
await sendService.sendText(
|
|
4059
|
+
sendCtx,
|
|
4060
|
+
nativeForwardMessageId ? '' : text,
|
|
4061
|
+
Object.keys(sendOptions).length > 0 ? sendOptions : undefined,
|
|
4062
|
+
);
|
|
2419
4063
|
}
|
|
2420
4064
|
},
|
|
2421
4065
|
onReplyStart: async () => {
|
|
@@ -2437,11 +4081,11 @@ export const bitrix24Plugin = {
|
|
|
2437
4081
|
}
|
|
2438
4082
|
|
|
2439
4083
|
if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
);
|
|
4084
|
+
logger.warn('Reply completed without a final user-visible message; fallback notice suppressed', {
|
|
4085
|
+
senderId: msgCtx.senderId,
|
|
4086
|
+
chatId: msgCtx.chatId,
|
|
4087
|
+
counts: dispatchResult.counts,
|
|
4088
|
+
});
|
|
2445
4089
|
} else if (replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
2446
4090
|
logger.debug('Late reply arrived during fallback grace window, skipping fallback', {
|
|
2447
4091
|
senderId: msgCtx.senderId,
|
|
@@ -2456,11 +4100,10 @@ export const bitrix24Plugin = {
|
|
|
2456
4100
|
}
|
|
2457
4101
|
|
|
2458
4102
|
if (!replyDelivered && dispatchFailed) {
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
);
|
|
4103
|
+
logger.warn('Reply dispatch failed before any user-visible message; fallback notice suppressed', {
|
|
4104
|
+
senderId: msgCtx.senderId,
|
|
4105
|
+
chatId: msgCtx.chatId,
|
|
4106
|
+
});
|
|
2464
4107
|
}
|
|
2465
4108
|
} finally {
|
|
2466
4109
|
await mediaService.cleanupDownloadedMedia(downloadedMedia.map((mediaItem) => mediaItem.path));
|
|
@@ -3086,12 +4729,19 @@ export const bitrix24Plugin = {
|
|
|
3086
4729
|
accountId: ctx.accountId,
|
|
3087
4730
|
peer: conversation.peer,
|
|
3088
4731
|
});
|
|
4732
|
+
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
4733
|
+
const nativeReplyAgentHint = buildBitrix24NativeReplyAgentHint(commandMessageId);
|
|
4734
|
+
const commandBodyForAgent = [
|
|
4735
|
+
fileDeliveryAgentHint,
|
|
4736
|
+
nativeReplyAgentHint,
|
|
4737
|
+
commandText,
|
|
4738
|
+
].filter(Boolean).join('\n\n');
|
|
3089
4739
|
|
|
3090
4740
|
const slashSessionKey = `bitrix24:slash:${senderId}:${Date.now()}`;
|
|
3091
4741
|
|
|
3092
4742
|
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
3093
4743
|
Body: commandText,
|
|
3094
|
-
BodyForAgent:
|
|
4744
|
+
BodyForAgent: commandBodyForAgent,
|
|
3095
4745
|
RawBody: commandText,
|
|
3096
4746
|
CommandBody: commandText,
|
|
3097
4747
|
CommandAuthorized: true,
|
|
@@ -3131,20 +4781,29 @@ export const bitrix24Plugin = {
|
|
|
3131
4781
|
deliver: async (payload) => {
|
|
3132
4782
|
await replyStatusHeartbeat.stopAndWait();
|
|
3133
4783
|
if (payload.text) {
|
|
4784
|
+
const replyDirective = extractReplyDirective({
|
|
4785
|
+
text: payload.text,
|
|
4786
|
+
currentMessageId: commandMessageId,
|
|
4787
|
+
explicitReplyToId: (payload as Record<string, unknown>).replyToId,
|
|
4788
|
+
});
|
|
3134
4789
|
const keyboard = extractKeyboardFromPayload(payload)
|
|
3135
4790
|
?? defaultSessionKeyboard;
|
|
3136
4791
|
const formattedPayload = normalizeCommandReplyPayload({
|
|
3137
4792
|
commandName,
|
|
3138
4793
|
commandParams,
|
|
3139
|
-
text:
|
|
4794
|
+
text: replyDirective.cleanText,
|
|
3140
4795
|
language: cmdCtx.language,
|
|
3141
4796
|
});
|
|
4797
|
+
const sendOptions = {
|
|
4798
|
+
keyboard,
|
|
4799
|
+
convertMarkdown: formattedPayload.convertMarkdown,
|
|
4800
|
+
...(replyDirective.replyToMessageId ? { replyToMessageId: replyDirective.replyToMessageId } : {}),
|
|
4801
|
+
};
|
|
3142
4802
|
if (!commandReplyDelivered) {
|
|
3143
|
-
if (isDm) {
|
|
4803
|
+
if (isDm || replyDirective.replyToMessageId) {
|
|
3144
4804
|
commandReplyDelivered = true;
|
|
3145
4805
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
3146
|
-
|
|
3147
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
4806
|
+
...sendOptions,
|
|
3148
4807
|
});
|
|
3149
4808
|
} else {
|
|
3150
4809
|
commandReplyDelivered = true;
|
|
@@ -3157,10 +4816,7 @@ export const bitrix24Plugin = {
|
|
|
3157
4816
|
}
|
|
3158
4817
|
|
|
3159
4818
|
commandReplyDelivered = true;
|
|
3160
|
-
await sendService.sendText(sendCtx, formattedPayload.text,
|
|
3161
|
-
keyboard,
|
|
3162
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
3163
|
-
});
|
|
4819
|
+
await sendService.sendText(sendCtx, formattedPayload.text, sendOptions);
|
|
3164
4820
|
}
|
|
3165
4821
|
},
|
|
3166
4822
|
onReplyStart: async () => {
|
|
@@ -3183,18 +4839,12 @@ export const bitrix24Plugin = {
|
|
|
3183
4839
|
}
|
|
3184
4840
|
|
|
3185
4841
|
if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
} else {
|
|
3193
|
-
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
3194
|
-
keyboard: defaultSessionKeyboard,
|
|
3195
|
-
convertMarkdown: false,
|
|
3196
|
-
});
|
|
3197
|
-
}
|
|
4842
|
+
logger.warn('Command reply completed without a final user-visible message; fallback notice suppressed', {
|
|
4843
|
+
commandName,
|
|
4844
|
+
senderId,
|
|
4845
|
+
dialogId,
|
|
4846
|
+
counts: dispatchResult.counts,
|
|
4847
|
+
});
|
|
3198
4848
|
} else if (commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3199
4849
|
logger.debug('Late command reply arrived during fallback grace window, skipping fallback', {
|
|
3200
4850
|
commandName,
|
|
@@ -3210,18 +4860,11 @@ export const bitrix24Plugin = {
|
|
|
3210
4860
|
}
|
|
3211
4861
|
|
|
3212
4862
|
if (!commandReplyDelivered && commandDispatchFailed) {
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
});
|
|
3219
|
-
} else {
|
|
3220
|
-
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
3221
|
-
keyboard: defaultSessionKeyboard,
|
|
3222
|
-
convertMarkdown: false,
|
|
3223
|
-
});
|
|
3224
|
-
}
|
|
4863
|
+
logger.warn('Command reply dispatch failed before any user-visible message; fallback notice suppressed', {
|
|
4864
|
+
commandName,
|
|
4865
|
+
senderId,
|
|
4866
|
+
dialogId,
|
|
4867
|
+
});
|
|
3225
4868
|
}
|
|
3226
4869
|
},
|
|
3227
4870
|
|