@ihazz/bitrix24 1.1.12 → 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 +1253 -42
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.js +68 -68
- 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 +1734 -76
- package/src/i18n.ts +68 -68
- 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
|
@@ -52,8 +52,11 @@ import {
|
|
|
52
52
|
watchOwnerDmNotice,
|
|
53
53
|
} from './i18n.js';
|
|
54
54
|
import { HistoryCache } from './history-cache.js';
|
|
55
|
-
import type { ConversationMeta } from './history-cache.js';
|
|
55
|
+
import type { ConversationMeta, HistoryEntry } from './history-cache.js';
|
|
56
56
|
import type {
|
|
57
|
+
B24Attach,
|
|
58
|
+
B24AttachBlock,
|
|
59
|
+
B24AttachColorToken,
|
|
57
60
|
B24MsgContext,
|
|
58
61
|
B24InputActionStatusCode,
|
|
59
62
|
B24V2FetchEventItem,
|
|
@@ -68,9 +71,12 @@ import type {
|
|
|
68
71
|
B24V2GetMessageResult,
|
|
69
72
|
B24V2User,
|
|
70
73
|
B24Keyboard,
|
|
74
|
+
ChannelMessageToolDiscovery,
|
|
75
|
+
ExtractedToolSendTarget,
|
|
71
76
|
KeyboardButton,
|
|
72
77
|
Logger,
|
|
73
78
|
} from './types.js';
|
|
79
|
+
import { markdownToBbCode } from './message-utils.js';
|
|
74
80
|
|
|
75
81
|
const PHASE_STATUS_DURATION_SECONDS = 8;
|
|
76
82
|
const PHASE_STATUS_REFRESH_GRACE_MS = 1000;
|
|
@@ -93,7 +99,24 @@ const FORWARDED_CONTEXT_RANGE = 5;
|
|
|
93
99
|
const ACTIVE_SESSION_NAMESPACE_TTL_MS = 180 * 24 * 60 * 60 * 1000;
|
|
94
100
|
const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
|
|
95
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;
|
|
96
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
|
+
}>();
|
|
97
120
|
|
|
98
121
|
// ─── Emoji → B24 reaction code mapping ──────────────────────────────────
|
|
99
122
|
// B24 uses named reaction codes, not Unicode emoji.
|
|
@@ -169,6 +192,606 @@ function toMessageId(value: string | number | undefined): number | undefined {
|
|
|
169
192
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
170
193
|
}
|
|
171
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
|
+
|
|
172
795
|
function escapeBbCodeText(value: string): string {
|
|
173
796
|
return value
|
|
174
797
|
.replace(/\[/g, '(')
|
|
@@ -1050,51 +1673,484 @@ interface GatewayState {
|
|
|
1050
1673
|
eventMode: 'fetch' | 'webhook';
|
|
1051
1674
|
}
|
|
1052
1675
|
|
|
1053
|
-
let gatewayState: GatewayState | null = null;
|
|
1676
|
+
let gatewayState: GatewayState | null = null;
|
|
1677
|
+
|
|
1678
|
+
export function __setGatewayStateForTests(state: GatewayState | null): void {
|
|
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
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// ─── Keyboard layouts ────────────────────────────────────────────────────────
|
|
1705
|
+
|
|
1706
|
+
export function buildWelcomeKeyboard(language?: string): B24Keyboard {
|
|
1707
|
+
const labels = welcomeKeyboardLabels(language);
|
|
1708
|
+
|
|
1709
|
+
return [
|
|
1710
|
+
{ TEXT: labels.todayTasks, ACTION: 'SEND', ACTION_VALUE: labels.todayTasks, DISPLAY: 'LINE' },
|
|
1711
|
+
{ TEXT: labels.stalledDeals, ACTION: 'SEND', ACTION_VALUE: labels.stalledDeals, DISPLAY: 'LINE' },
|
|
1712
|
+
{ TYPE: 'NEWLINE' },
|
|
1713
|
+
{ TEXT: labels.newSession, COMMAND: 'new', DISPLAY: 'LINE' },
|
|
1714
|
+
{ TEXT: labels.commands, COMMAND: 'commands', DISPLAY: 'LINE' },
|
|
1715
|
+
{ TEXT: labels.help, COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
|
|
1716
|
+
];
|
|
1717
|
+
}
|
|
1718
|
+
|
|
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
|
+
}
|
|
1054
2090
|
|
|
1055
|
-
|
|
1056
|
-
gatewayState = state;
|
|
2091
|
+
return undefined;
|
|
1057
2092
|
}
|
|
1058
2093
|
|
|
1059
|
-
|
|
2094
|
+
function collectActionMediaUrls(params: Record<string, unknown>): string[] {
|
|
2095
|
+
const mediaUrls: string[] = [];
|
|
2096
|
+
const seen = new Set<string>();
|
|
1060
2097
|
|
|
1061
|
-
|
|
1062
|
-
|
|
2098
|
+
const append = (value: unknown) => {
|
|
2099
|
+
if (Array.isArray(value)) {
|
|
2100
|
+
value.forEach(append);
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
1063
2103
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
{ TYPE: 'NEWLINE' },
|
|
1068
|
-
{ TEXT: labels.newSession, COMMAND: 'new', DISPLAY: 'LINE' },
|
|
1069
|
-
{ TEXT: labels.commands, COMMAND: 'commands', DISPLAY: 'LINE' },
|
|
1070
|
-
{ TEXT: labels.help, COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
|
|
1071
|
-
];
|
|
1072
|
-
}
|
|
2104
|
+
if (typeof value !== 'string') {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
1073
2107
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
2108
|
+
const trimmed = value.trim();
|
|
2109
|
+
if (!trimmed) {
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
1077
2112
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
};
|
|
2127
|
+
|
|
2128
|
+
append(params.mediaUrl);
|
|
2129
|
+
append(params.mediaUrls);
|
|
2130
|
+
append(params.filePath);
|
|
2131
|
+
append(params.filePaths);
|
|
2132
|
+
|
|
2133
|
+
return mediaUrls;
|
|
1086
2134
|
}
|
|
1087
2135
|
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
+
}
|
|
1090
2148
|
|
|
1091
|
-
|
|
2149
|
+
if (params.attach) {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
1092
2152
|
|
|
1093
|
-
|
|
1094
|
-
export interface ChannelButton {
|
|
1095
|
-
text: string;
|
|
1096
|
-
callback_data?: string;
|
|
1097
|
-
style?: string;
|
|
2153
|
+
return params.keyboard ? ' ' : null;
|
|
1098
2154
|
}
|
|
1099
2155
|
|
|
1100
2156
|
function parseRegisteredCommandTrigger(callbackData: string): { command: string; commandParams?: string } | undefined {
|
|
@@ -1187,6 +2243,16 @@ export function extractKeyboardFromPayload(
|
|
|
1187
2243
|
return undefined;
|
|
1188
2244
|
}
|
|
1189
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
|
+
|
|
1190
2256
|
/**
|
|
1191
2257
|
* Extract inline button JSON embedded in message text by the agent.
|
|
1192
2258
|
*
|
|
@@ -1220,6 +2286,14 @@ export function extractInlineButtonsFromText(
|
|
|
1220
2286
|
}
|
|
1221
2287
|
}
|
|
1222
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
|
+
|
|
1223
2297
|
function normalizeCommandReplyPayload(params: {
|
|
1224
2298
|
commandName: string;
|
|
1225
2299
|
commandParams: string;
|
|
@@ -1577,11 +2651,12 @@ function resolveOutboundSendCtx(params: {
|
|
|
1577
2651
|
accountId?: string;
|
|
1578
2652
|
}): SendContext | null {
|
|
1579
2653
|
const { config } = resolveAccount(params.cfg, params.accountId);
|
|
1580
|
-
|
|
2654
|
+
const dialogId = normalizeBitrix24DialogTarget(params.to);
|
|
2655
|
+
if (!config.webhookUrl || !gatewayState || !dialogId) return null;
|
|
1581
2656
|
return {
|
|
1582
2657
|
webhookUrl: config.webhookUrl,
|
|
1583
2658
|
bot: gatewayState.bot,
|
|
1584
|
-
dialogId
|
|
2659
|
+
dialogId,
|
|
1585
2660
|
};
|
|
1586
2661
|
}
|
|
1587
2662
|
|
|
@@ -1665,13 +2740,10 @@ export const bitrix24Plugin = {
|
|
|
1665
2740
|
},
|
|
1666
2741
|
|
|
1667
2742
|
messaging: {
|
|
1668
|
-
normalizeTarget: (raw: string) => raw
|
|
2743
|
+
normalizeTarget: (raw: string) => normalizeBitrix24DialogTarget(raw),
|
|
1669
2744
|
targetResolver: {
|
|
1670
|
-
hint: 'Use a numeric
|
|
1671
|
-
looksLikeId: (raw: string, _normalized: string) =>
|
|
1672
|
-
const stripped = raw.trim().replace(CHANNEL_PREFIX_RE, '');
|
|
1673
|
-
return /^\d+$/.test(stripped);
|
|
1674
|
-
},
|
|
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),
|
|
1675
2747
|
},
|
|
1676
2748
|
},
|
|
1677
2749
|
|
|
@@ -1786,25 +2858,46 @@ export const bitrix24Plugin = {
|
|
|
1786
2858
|
const sendCtx = resolveOutboundSendCtx(ctx);
|
|
1787
2859
|
if (!sendCtx || !gatewayState) throw new Error('Bitrix24 gateway not started');
|
|
1788
2860
|
|
|
2861
|
+
const text = ctx.text;
|
|
2862
|
+
|
|
1789
2863
|
const keyboard = ctx.payload?.channelData
|
|
1790
2864
|
? extractKeyboardFromPayload({ channelData: ctx.payload.channelData })
|
|
1791
2865
|
: undefined;
|
|
2866
|
+
const attach = ctx.payload?.channelData
|
|
2867
|
+
? extractAttachFromPayload({ channelData: ctx.payload.channelData })
|
|
2868
|
+
: undefined;
|
|
1792
2869
|
const mediaUrls = collectOutboundMediaUrls({
|
|
1793
2870
|
mediaUrl: ctx.mediaUrl,
|
|
1794
2871
|
payload: ctx.payload as { mediaUrl?: string; mediaUrls?: string[] } | undefined,
|
|
1795
2872
|
});
|
|
1796
2873
|
|
|
1797
2874
|
if (mediaUrls.length > 0) {
|
|
2875
|
+
const initialMessage = !keyboard && !attach ? text || undefined : undefined;
|
|
1798
2876
|
const uploadedMessageId = await uploadOutboundMedia({
|
|
1799
2877
|
mediaService: gatewayState.mediaService,
|
|
1800
2878
|
sendCtx,
|
|
1801
2879
|
mediaUrls,
|
|
2880
|
+
initialMessage,
|
|
1802
2881
|
});
|
|
1803
2882
|
|
|
1804
|
-
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
|
+
|
|
1805
2898
|
const result = await gatewayState.sendService.sendText(
|
|
1806
2899
|
sendCtx,
|
|
1807
|
-
|
|
2900
|
+
text || '',
|
|
1808
2901
|
keyboard ? { keyboard } : undefined,
|
|
1809
2902
|
);
|
|
1810
2903
|
return { messageId: String(result.messageId ?? uploadedMessageId) };
|
|
@@ -1813,10 +2906,24 @@ export const bitrix24Plugin = {
|
|
|
1813
2906
|
return { messageId: uploadedMessageId };
|
|
1814
2907
|
}
|
|
1815
2908
|
|
|
1816
|
-
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
|
+
|
|
1817
2924
|
const result = await gatewayState.sendService.sendText(
|
|
1818
2925
|
sendCtx,
|
|
1819
|
-
|
|
2926
|
+
text || '',
|
|
1820
2927
|
keyboard ? { keyboard } : undefined,
|
|
1821
2928
|
);
|
|
1822
2929
|
return { messageId: String(result.messageId ?? '') };
|
|
@@ -1828,12 +2935,23 @@ export const bitrix24Plugin = {
|
|
|
1828
2935
|
// ─── Actions (agent-driven: reactions, etc.) ────────────────────────────
|
|
1829
2936
|
|
|
1830
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
|
+
|
|
1831
2947
|
listActions: (_params: { cfg: Record<string, unknown> }): string[] => {
|
|
1832
|
-
return [
|
|
2948
|
+
return [...BITRIX24_DISCOVERY_ACTION_NAMES];
|
|
1833
2949
|
},
|
|
1834
2950
|
|
|
1835
2951
|
supportsAction: (params: { action: string }): boolean => {
|
|
1836
|
-
return
|
|
2952
|
+
return BITRIX24_ACTION_NAMES.includes(
|
|
2953
|
+
params.action as typeof BITRIX24_ACTION_NAMES[number],
|
|
2954
|
+
);
|
|
1837
2955
|
},
|
|
1838
2956
|
|
|
1839
2957
|
handleAction: async (ctx: {
|
|
@@ -1852,35 +2970,270 @@ export const bitrix24Plugin = {
|
|
|
1852
2970
|
|
|
1853
2971
|
// ─── Send with buttons ──────────────────────────────────────────────
|
|
1854
2972
|
if (ctx.action === 'send') {
|
|
1855
|
-
// Only intercept send when buttons are present; otherwise let gateway handle normally
|
|
1856
2973
|
const rawButtons = ctx.params.buttons;
|
|
1857
|
-
|
|
2974
|
+
const rawAttach = ctx.params.attach ?? ctx.params.attachments;
|
|
1858
2975
|
|
|
1859
2976
|
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
1860
2977
|
if (!config.webhookUrl || !gatewayState) return null;
|
|
1861
2978
|
|
|
1862
2979
|
const bot = gatewayState.bot;
|
|
1863
|
-
const to =
|
|
2980
|
+
const to = readActionTargetParam(ctx.params, ctx.to);
|
|
1864
2981
|
if (!to) {
|
|
1865
2982
|
defaultLogger.warn('handleAction send: no "to" in params or ctx, falling back to gateway');
|
|
1866
2983
|
return null;
|
|
1867
2984
|
}
|
|
1868
2985
|
|
|
1869
2986
|
const sendCtx: SendContext = { webhookUrl: config.webhookUrl, bot, dialogId: to };
|
|
1870
|
-
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
|
+
}
|
|
1871
3028
|
|
|
1872
|
-
// Parse buttons: may be array or JSON string
|
|
1873
|
-
let buttons: ChannelButton[][] | undefined;
|
|
1874
3029
|
try {
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
+
}
|
|
1880
3076
|
|
|
1881
|
-
|
|
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
|
+
}
|
|
1882
3236
|
|
|
1883
|
-
try {
|
|
1884
3237
|
const result = await gatewayState.sendService.sendText(
|
|
1885
3238
|
sendCtx, message || ' ', keyboard ? { keyboard } : undefined,
|
|
1886
3239
|
);
|
|
@@ -1897,6 +3250,192 @@ export const bitrix24Plugin = {
|
|
|
1897
3250
|
}
|
|
1898
3251
|
}
|
|
1899
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
|
+
|
|
1900
3439
|
// ─── React ────────────────────────────────────────────────────────────
|
|
1901
3440
|
if (ctx.action !== 'react') return null;
|
|
1902
3441
|
|
|
@@ -1954,11 +3493,13 @@ export const bitrix24Plugin = {
|
|
|
1954
3493
|
|
|
1955
3494
|
try {
|
|
1956
3495
|
await api.addReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
3496
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
1957
3497
|
return toolResult({ ok: true, added: emoji });
|
|
1958
3498
|
} catch (err) {
|
|
1959
3499
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1960
3500
|
const isAlreadySet = errMsg.includes('REACTION_ALREADY_SET');
|
|
1961
3501
|
if (isAlreadySet) {
|
|
3502
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
1962
3503
|
return toolResult({ ok: true, added: emoji, warning: 'Reaction already set.' });
|
|
1963
3504
|
}
|
|
1964
3505
|
return toolResult({ ok: false, reason: 'error', emoji, hint: `Reaction failed: ${errMsg}. Do not retry.` });
|
|
@@ -2326,9 +3867,22 @@ export const bitrix24Plugin = {
|
|
|
2326
3867
|
query: bodyWithReply,
|
|
2327
3868
|
historyCache,
|
|
2328
3869
|
});
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
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');
|
|
2332
3886
|
const combinedBody = msgCtx.isGroup
|
|
2333
3887
|
? buildHistoryContext({
|
|
2334
3888
|
entries: previousEntries,
|
|
@@ -2337,6 +3891,13 @@ export const bitrix24Plugin = {
|
|
|
2337
3891
|
: bodyForAgent;
|
|
2338
3892
|
|
|
2339
3893
|
recordHistory(body);
|
|
3894
|
+
rememberRecentInboundMessage(
|
|
3895
|
+
conversation.dialogId,
|
|
3896
|
+
msgCtx.messageId,
|
|
3897
|
+
body,
|
|
3898
|
+
msgCtx.timestamp ?? Date.now(),
|
|
3899
|
+
ctx.accountId,
|
|
3900
|
+
);
|
|
2340
3901
|
|
|
2341
3902
|
// Resolve which agent handles this conversation
|
|
2342
3903
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
@@ -2401,7 +3962,31 @@ export const bitrix24Plugin = {
|
|
|
2401
3962
|
});
|
|
2402
3963
|
}
|
|
2403
3964
|
if (payload.text) {
|
|
2404
|
-
|
|
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
|
+
}
|
|
2405
3990
|
let keyboard = extractKeyboardFromPayload(payload);
|
|
2406
3991
|
|
|
2407
3992
|
// Fallback: agent may embed button JSON in text as [[{...}]]
|
|
@@ -2413,8 +3998,68 @@ export const bitrix24Plugin = {
|
|
|
2413
3998
|
}
|
|
2414
3999
|
}
|
|
2415
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
|
+
|
|
2416
4026
|
replyDelivered = true;
|
|
2417
|
-
|
|
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
|
+
);
|
|
2418
4063
|
}
|
|
2419
4064
|
},
|
|
2420
4065
|
onReplyStart: async () => {
|
|
@@ -3084,12 +4729,19 @@ export const bitrix24Plugin = {
|
|
|
3084
4729
|
accountId: ctx.accountId,
|
|
3085
4730
|
peer: conversation.peer,
|
|
3086
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');
|
|
3087
4739
|
|
|
3088
4740
|
const slashSessionKey = `bitrix24:slash:${senderId}:${Date.now()}`;
|
|
3089
4741
|
|
|
3090
4742
|
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
3091
4743
|
Body: commandText,
|
|
3092
|
-
BodyForAgent:
|
|
4744
|
+
BodyForAgent: commandBodyForAgent,
|
|
3093
4745
|
RawBody: commandText,
|
|
3094
4746
|
CommandBody: commandText,
|
|
3095
4747
|
CommandAuthorized: true,
|
|
@@ -3129,20 +4781,29 @@ export const bitrix24Plugin = {
|
|
|
3129
4781
|
deliver: async (payload) => {
|
|
3130
4782
|
await replyStatusHeartbeat.stopAndWait();
|
|
3131
4783
|
if (payload.text) {
|
|
4784
|
+
const replyDirective = extractReplyDirective({
|
|
4785
|
+
text: payload.text,
|
|
4786
|
+
currentMessageId: commandMessageId,
|
|
4787
|
+
explicitReplyToId: (payload as Record<string, unknown>).replyToId,
|
|
4788
|
+
});
|
|
3132
4789
|
const keyboard = extractKeyboardFromPayload(payload)
|
|
3133
4790
|
?? defaultSessionKeyboard;
|
|
3134
4791
|
const formattedPayload = normalizeCommandReplyPayload({
|
|
3135
4792
|
commandName,
|
|
3136
4793
|
commandParams,
|
|
3137
|
-
text:
|
|
4794
|
+
text: replyDirective.cleanText,
|
|
3138
4795
|
language: cmdCtx.language,
|
|
3139
4796
|
});
|
|
4797
|
+
const sendOptions = {
|
|
4798
|
+
keyboard,
|
|
4799
|
+
convertMarkdown: formattedPayload.convertMarkdown,
|
|
4800
|
+
...(replyDirective.replyToMessageId ? { replyToMessageId: replyDirective.replyToMessageId } : {}),
|
|
4801
|
+
};
|
|
3140
4802
|
if (!commandReplyDelivered) {
|
|
3141
|
-
if (isDm) {
|
|
4803
|
+
if (isDm || replyDirective.replyToMessageId) {
|
|
3142
4804
|
commandReplyDelivered = true;
|
|
3143
4805
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
3144
|
-
|
|
3145
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
4806
|
+
...sendOptions,
|
|
3146
4807
|
});
|
|
3147
4808
|
} else {
|
|
3148
4809
|
commandReplyDelivered = true;
|
|
@@ -3155,10 +4816,7 @@ export const bitrix24Plugin = {
|
|
|
3155
4816
|
}
|
|
3156
4817
|
|
|
3157
4818
|
commandReplyDelivered = true;
|
|
3158
|
-
await sendService.sendText(sendCtx, formattedPayload.text,
|
|
3159
|
-
keyboard,
|
|
3160
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
3161
|
-
});
|
|
4819
|
+
await sendService.sendText(sendCtx, formattedPayload.text, sendOptions);
|
|
3162
4820
|
}
|
|
3163
4821
|
},
|
|
3164
4822
|
onReplyStart: async () => {
|