@ihazz/bitrix24 1.1.12 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +20 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +2303 -81
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.d.ts +1 -0
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +79 -68
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/inbound-handler.d.ts +10 -0
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/dist/src/inbound-handler.js +281 -16
- package/dist/src/inbound-handler.js.map +1 -1
- package/dist/src/media-service.d.ts +4 -0
- package/dist/src/media-service.d.ts.map +1 -1
- package/dist/src/media-service.js +147 -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 +113 -4
- 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 +2 -1
- package/dist/src/send-service.d.ts.map +1 -1
- package/dist/src/send-service.js +34 -5
- 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 +3746 -843
- package/src/i18n.ts +81 -68
- package/src/inbound-handler.ts +357 -17
- package/src/media-service.ts +185 -15
- package/src/message-utils.ts +144 -4
- package/src/runtime.ts +1 -0
- package/src/send-service.ts +52 -4
- package/src/state-paths.ts +11 -0
- package/src/types.ts +122 -0
package/dist/src/channel.js
CHANGED
|
@@ -14,8 +14,9 @@ import { DEFAULT_AVATAR_BASE64 } from './bot-avatar.js';
|
|
|
14
14
|
import { Bitrix24ApiError, createVerboseLogger, defaultLogger, CHANNEL_PREFIX_RE } from './utils.js';
|
|
15
15
|
import { getBitrix24Runtime } from './runtime.js';
|
|
16
16
|
import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply, getCommandRegistrationPayload, } from './commands.js';
|
|
17
|
-
import { accessApproved, accessDenied, commandKeyboardLabels, groupPairingPending, mediaDownloadFailed, groupChatUnsupported, newSessionReplyTexts, onboardingDisclaimerMessage, onboardingMessage, normalizeNewSessionReply, ownerAndAllowedUsersOnly, personalBotOwnerOnly, welcomeKeyboardLabels, watchOwnerDmNotice, } from './i18n.js';
|
|
17
|
+
import { accessApproved, accessDenied, commandKeyboardLabels, emptyReplyFallback, groupPairingPending, mediaDownloadFailed, groupChatUnsupported, newSessionReplyTexts, onboardingDisclaimerMessage, onboardingMessage, normalizeNewSessionReply, ownerAndAllowedUsersOnly, personalBotOwnerOnly, welcomeKeyboardLabels, watchOwnerDmNotice, } from './i18n.js';
|
|
18
18
|
import { HistoryCache } from './history-cache.js';
|
|
19
|
+
import { markdownToBbCode } from './message-utils.js';
|
|
19
20
|
const PHASE_STATUS_DURATION_SECONDS = 8;
|
|
20
21
|
const PHASE_STATUS_REFRESH_GRACE_MS = 1000;
|
|
21
22
|
const THINKING_STATUS_DURATION_SECONDS = 30;
|
|
@@ -23,6 +24,7 @@ const THINKING_STATUS_REFRESH_GRACE_MS = 6000;
|
|
|
23
24
|
const DIRECT_TEXT_COALESCE_DEBOUNCE_MS = 200;
|
|
24
25
|
const DIRECT_TEXT_COALESCE_MAX_WAIT_MS = 5000;
|
|
25
26
|
const REPLY_FALLBACK_GRACE_MS = 750;
|
|
27
|
+
const EMPTY_REPLY_FALLBACK_GRACE_MS = 15 * 1000;
|
|
26
28
|
const ACCESS_DENIED_NOTICE_COOLDOWN_MS = 60000;
|
|
27
29
|
const AUTO_BOT_CODE_MAX_CANDIDATES = 100;
|
|
28
30
|
const MEDIA_DOWNLOAD_CONCURRENCY = 2;
|
|
@@ -37,7 +39,27 @@ const FORWARDED_CONTEXT_RANGE = 5;
|
|
|
37
39
|
const ACTIVE_SESSION_NAMESPACE_TTL_MS = 180 * 24 * 60 * 60 * 1000;
|
|
38
40
|
const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
|
|
39
41
|
const ACTIVE_SESSION_NAMESPACE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
42
|
+
const NATIVE_FORWARD_ACK_TTL_MS = 60 * 1000;
|
|
43
|
+
const NATIVE_REACTION_ACK_TTL_MS = 60 * 1000;
|
|
44
|
+
const NATIVE_FORWARD_SUCCESS_REACTION = 'whiteHeavyCheckMark';
|
|
45
|
+
const REACTION_TYPING_RESET_DURATION_SECONDS = 1;
|
|
46
|
+
const GENERIC_OUTBOUND_FILE_NAME_RE = /^(translated?|translation|output|result|file|document)(?:[_-][a-z]{2,8})?$/i;
|
|
47
|
+
const GENERIC_INBOUND_FILE_NAME_RE = /^(?:file[_-]\d+|[0-9a-f]{8,}(?:-[0-9a-f]{4,})*(?:_file[_-]?\d+)?)$/i;
|
|
48
|
+
const RECENT_MEDIA_DELIVERY_TTL_MS = 60 * 1000;
|
|
49
|
+
const RECENT_SUCCESSFUL_ACTION_TTL_MS = 60 * 1000;
|
|
50
|
+
const RECENT_OUTBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
51
|
+
const RECENT_WATCH_NOTIFICATION_TTL_MS = 30 * 60 * 1000;
|
|
52
|
+
const RECENT_INBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
53
|
+
const RECENT_INBOUND_MESSAGE_LIMIT = 20;
|
|
40
54
|
const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
|
|
55
|
+
const pendingNativeForwardAcks = new Map();
|
|
56
|
+
const pendingNativeReactionAcks = new Map();
|
|
57
|
+
const recentSuccessfulMediaDeliveries = new Map();
|
|
58
|
+
const recentSuccessfulActionsByMessage = new Map();
|
|
59
|
+
const recentOutboundMessagesById = new Map();
|
|
60
|
+
const recentWatchNotificationsByMessage = new Map();
|
|
61
|
+
const recentInboundMessagesByDialog = new Map();
|
|
62
|
+
const inboundMessageContextById = new Map();
|
|
41
63
|
// ─── Emoji → B24 reaction code mapping ──────────────────────────────────
|
|
42
64
|
// B24 uses named reaction codes, not Unicode emoji.
|
|
43
65
|
// Map common Unicode emoji to their B24 equivalents.
|
|
@@ -109,6 +131,789 @@ function toMessageId(value) {
|
|
|
109
131
|
const parsed = Number(value);
|
|
110
132
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
111
133
|
}
|
|
134
|
+
function resolveStateAccountId(accountId) {
|
|
135
|
+
const normalizedAccountId = typeof accountId === 'string'
|
|
136
|
+
? accountId.trim()
|
|
137
|
+
: '';
|
|
138
|
+
if (normalizedAccountId) {
|
|
139
|
+
return normalizedAccountId;
|
|
140
|
+
}
|
|
141
|
+
const gatewayAccountId = gatewayState?.accountId?.trim();
|
|
142
|
+
return gatewayAccountId || 'default';
|
|
143
|
+
}
|
|
144
|
+
function buildAccountDialogScopeKey(accountId, dialogId) {
|
|
145
|
+
const normalizedDialogId = dialogId.trim();
|
|
146
|
+
if (!normalizedDialogId) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return `${resolveStateAccountId(accountId)}:${normalizedDialogId}`;
|
|
150
|
+
}
|
|
151
|
+
function buildAccountMessageScopeKey(accountId, currentMessageId) {
|
|
152
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
153
|
+
if (!normalizedMessageId) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return `${resolveStateAccountId(accountId)}:${normalizedMessageId}`;
|
|
157
|
+
}
|
|
158
|
+
function buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId) {
|
|
159
|
+
const dialogKey = buildAccountDialogScopeKey(accountId, dialogId);
|
|
160
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
161
|
+
if (!dialogKey || !normalizedMessageId) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
return `${dialogKey}:${normalizedMessageId}`;
|
|
165
|
+
}
|
|
166
|
+
function prunePendingNativeForwardAcks(now = Date.now()) {
|
|
167
|
+
for (const [key, expiresAt] of pendingNativeForwardAcks.entries()) {
|
|
168
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
169
|
+
pendingNativeForwardAcks.delete(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function markPendingNativeForwardAck(dialogId, currentMessageId, accountId) {
|
|
174
|
+
const key = buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
175
|
+
if (!key) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
prunePendingNativeForwardAcks();
|
|
179
|
+
pendingNativeForwardAcks.set(key, Date.now() + NATIVE_FORWARD_ACK_TTL_MS);
|
|
180
|
+
}
|
|
181
|
+
function consumePendingNativeForwardAck(dialogId, currentMessageId, accountId) {
|
|
182
|
+
const key = buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
183
|
+
if (!key) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
prunePendingNativeForwardAcks();
|
|
187
|
+
const expiresAt = pendingNativeForwardAcks.get(key);
|
|
188
|
+
if (!expiresAt) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
pendingNativeForwardAcks.delete(key);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
function resolvePendingNativeForwardAckDialogId(currentMessageId, fallbackDialogId, accountId) {
|
|
195
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
196
|
+
if (!messageKey) {
|
|
197
|
+
return fallbackDialogId;
|
|
198
|
+
}
|
|
199
|
+
return inboundMessageContextById.get(messageKey)?.dialogId ?? fallbackDialogId;
|
|
200
|
+
}
|
|
201
|
+
function resolveRecentInboundDialogId(currentMessageId, accountId) {
|
|
202
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
203
|
+
if (!messageKey) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
pruneRecentInboundMessages();
|
|
207
|
+
return inboundMessageContextById.get(messageKey)?.dialogId;
|
|
208
|
+
}
|
|
209
|
+
function splitFileNameParts(fileName) {
|
|
210
|
+
const normalizedFileName = basename(fileName).trim();
|
|
211
|
+
const lastDotIndex = normalizedFileName.lastIndexOf('.');
|
|
212
|
+
if (lastDotIndex <= 0) {
|
|
213
|
+
return { stem: normalizedFileName, extension: '' };
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
stem: normalizedFileName.slice(0, lastDotIndex).trim(),
|
|
217
|
+
extension: normalizedFileName.slice(lastDotIndex).trim(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function normalizeSourceFileStem(stem) {
|
|
221
|
+
return stem
|
|
222
|
+
.trim()
|
|
223
|
+
.replace(/\s+\(\d+\)$/u, '')
|
|
224
|
+
.trim();
|
|
225
|
+
}
|
|
226
|
+
function isGenericOutboundFileName(fileName) {
|
|
227
|
+
const { stem } = splitFileNameParts(fileName);
|
|
228
|
+
return GENERIC_OUTBOUND_FILE_NAME_RE.test(stem);
|
|
229
|
+
}
|
|
230
|
+
function isSemanticInboundFileName(fileName) {
|
|
231
|
+
const { stem } = splitFileNameParts(fileName);
|
|
232
|
+
return Boolean(stem) && !GENERIC_INBOUND_FILE_NAME_RE.test(stem);
|
|
233
|
+
}
|
|
234
|
+
function inferTranslationSuffixFromBody(body) {
|
|
235
|
+
const normalizedBody = body.toLowerCase();
|
|
236
|
+
if (!/translate|translation|перевед|перевод/u.test(normalizedBody)) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const languagePatterns = [
|
|
240
|
+
['en', /english|английск/u],
|
|
241
|
+
['ru', /russian|русск/u],
|
|
242
|
+
['de', /german|немец/u],
|
|
243
|
+
['fr', /french|француз/u],
|
|
244
|
+
['es', /spanish|испан/u],
|
|
245
|
+
['it', /italian|итальян/u],
|
|
246
|
+
['pt', /portuguese|португал/u],
|
|
247
|
+
['zh', /chinese|китай/u],
|
|
248
|
+
['ja', /japanese|япон/u],
|
|
249
|
+
['ko', /korean|корей/u],
|
|
250
|
+
];
|
|
251
|
+
for (const [code, pattern] of languagePatterns) {
|
|
252
|
+
if (pattern.test(normalizedBody)) {
|
|
253
|
+
return `_${code}`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return '_translated';
|
|
257
|
+
}
|
|
258
|
+
function inferTranslationSuffix(requestedFileName, body) {
|
|
259
|
+
const { stem } = splitFileNameParts(requestedFileName);
|
|
260
|
+
const suffixMatch = stem.match(/(?:^|[_-])(en|ru|de|fr|es|it|pt|zh|ja|ko)$/i);
|
|
261
|
+
if (suffixMatch?.[1]) {
|
|
262
|
+
return `_${suffixMatch[1].toLowerCase()}`;
|
|
263
|
+
}
|
|
264
|
+
return inferTranslationSuffixFromBody(body);
|
|
265
|
+
}
|
|
266
|
+
function resolveRecentInboundContext(params) {
|
|
267
|
+
pruneRecentInboundMessages();
|
|
268
|
+
const explicitKey = buildAccountMessageScopeKey(params.accountId, params.currentMessageId);
|
|
269
|
+
const explicitEntry = explicitKey ? inboundMessageContextById.get(explicitKey) : undefined;
|
|
270
|
+
if (explicitEntry) {
|
|
271
|
+
return explicitEntry;
|
|
272
|
+
}
|
|
273
|
+
const dialogKey = buildAccountDialogScopeKey(params.accountId, params.dialogId);
|
|
274
|
+
if (!dialogKey) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
const recentEntries = recentInboundMessagesByDialog.get(dialogKey);
|
|
278
|
+
if (!recentEntries?.length) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
for (let index = recentEntries.length - 1; index >= 0; index -= 1) {
|
|
282
|
+
const entry = recentEntries[index];
|
|
283
|
+
const messageKey = buildAccountMessageScopeKey(params.accountId, entry.messageId);
|
|
284
|
+
if (!messageKey) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const inboundEntry = inboundMessageContextById.get(messageKey);
|
|
288
|
+
if (inboundEntry) {
|
|
289
|
+
return inboundEntry;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
function resolveSemanticOutboundFileName(params) {
|
|
295
|
+
const requestedBaseName = basename(params.requestedFileName).trim();
|
|
296
|
+
if (!requestedBaseName || !isGenericOutboundFileName(requestedBaseName)) {
|
|
297
|
+
return requestedBaseName;
|
|
298
|
+
}
|
|
299
|
+
const inboundContext = resolveRecentInboundContext({
|
|
300
|
+
dialogId: params.dialogId,
|
|
301
|
+
currentMessageId: params.currentMessageId,
|
|
302
|
+
accountId: params.accountId,
|
|
303
|
+
});
|
|
304
|
+
if (!inboundContext || inboundContext.mediaNames.length !== 1) {
|
|
305
|
+
return requestedBaseName;
|
|
306
|
+
}
|
|
307
|
+
const sourceFileName = inboundContext.mediaNames[0];
|
|
308
|
+
if (!isSemanticInboundFileName(sourceFileName)) {
|
|
309
|
+
return requestedBaseName;
|
|
310
|
+
}
|
|
311
|
+
const { stem: sourceStemRaw, extension: sourceExtension } = splitFileNameParts(sourceFileName);
|
|
312
|
+
const { extension: requestedExtension } = splitFileNameParts(requestedBaseName);
|
|
313
|
+
const normalizedSourceStem = normalizeSourceFileStem(sourceStemRaw);
|
|
314
|
+
if (!normalizedSourceStem) {
|
|
315
|
+
return requestedBaseName;
|
|
316
|
+
}
|
|
317
|
+
const suffix = inferTranslationSuffix(requestedBaseName, inboundContext.body) ?? '';
|
|
318
|
+
const nextStem = suffix && !normalizedSourceStem.endsWith(suffix)
|
|
319
|
+
? `${normalizedSourceStem}${suffix}`
|
|
320
|
+
: normalizedSourceStem;
|
|
321
|
+
const nextExtension = requestedExtension || sourceExtension;
|
|
322
|
+
return nextExtension ? `${nextStem}${nextExtension}` : nextStem;
|
|
323
|
+
}
|
|
324
|
+
async function maybeSendReactionTypingReset(params) {
|
|
325
|
+
const dialogId = typeof params.dialogId === 'string' ? params.dialogId.trim() : '';
|
|
326
|
+
if (!dialogId) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const sendTyping = params.sendService.sendTyping;
|
|
330
|
+
if (typeof sendTyping !== 'function') {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await sendTyping.call(params.sendService, { webhookUrl: params.webhookUrl, bot: params.bot, dialogId }, REACTION_TYPING_RESET_DURATION_SECONDS);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
defaultLogger.debug('Failed to send Bitrix24 reaction typing reset', {
|
|
338
|
+
dialogId,
|
|
339
|
+
error: error instanceof Error ? error.message : String(error),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function maybeAddNativeForwardSuccessReaction(params) {
|
|
344
|
+
const messageId = toMessageId(params.currentMessageId);
|
|
345
|
+
if (!messageId) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const addReaction = params.api.addReaction;
|
|
349
|
+
if (typeof addReaction !== 'function') {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let shouldResetTyping = false;
|
|
353
|
+
try {
|
|
354
|
+
await addReaction.call(params.api, params.webhookUrl, params.bot, messageId, NATIVE_FORWARD_SUCCESS_REACTION);
|
|
355
|
+
shouldResetTyping = true;
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
359
|
+
if (errorMessage.includes('REACTION_ALREADY_SET')) {
|
|
360
|
+
defaultLogger.debug('Native forward success reaction already set', { messageId });
|
|
361
|
+
shouldResetTyping = true;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
defaultLogger.warn('Failed to add native forward success reaction', {
|
|
365
|
+
messageId,
|
|
366
|
+
error: errorMessage,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (shouldResetTyping) {
|
|
371
|
+
await maybeSendReactionTypingReset({
|
|
372
|
+
sendService: params.sendService,
|
|
373
|
+
webhookUrl: params.webhookUrl,
|
|
374
|
+
bot: params.bot,
|
|
375
|
+
dialogId: params.dialogId,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function buildPendingNativeReactionAckKey(currentMessageId, accountId) {
|
|
380
|
+
return buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
381
|
+
}
|
|
382
|
+
function pruneRecentSuccessfulActions(now = Date.now()) {
|
|
383
|
+
for (const [key, expiresAt] of recentSuccessfulActionsByMessage.entries()) {
|
|
384
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
385
|
+
recentSuccessfulActionsByMessage.delete(key);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function markRecentSuccessfulAction(currentMessageId, accountId) {
|
|
390
|
+
const key = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
391
|
+
if (!key) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
pruneRecentSuccessfulActions();
|
|
395
|
+
recentSuccessfulActionsByMessage.set(key, Date.now() + RECENT_SUCCESSFUL_ACTION_TTL_MS);
|
|
396
|
+
}
|
|
397
|
+
function hasRecentSuccessfulAction(currentMessageId, accountId) {
|
|
398
|
+
const key = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
399
|
+
if (!key) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
pruneRecentSuccessfulActions();
|
|
403
|
+
return recentSuccessfulActionsByMessage.has(key);
|
|
404
|
+
}
|
|
405
|
+
function pruneRecentOutboundMessages(now = Date.now()) {
|
|
406
|
+
for (const [key, expiresAt] of recentOutboundMessagesById.entries()) {
|
|
407
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
408
|
+
recentOutboundMessagesById.delete(key);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function markRecentOutboundMessage(messageId, accountId) {
|
|
413
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
414
|
+
if (!key) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
pruneRecentOutboundMessages();
|
|
418
|
+
recentOutboundMessagesById.set(key, Date.now() + RECENT_OUTBOUND_MESSAGE_TTL_MS);
|
|
419
|
+
}
|
|
420
|
+
function hasRecentOutboundMessage(messageId, accountId) {
|
|
421
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
422
|
+
if (!key) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
pruneRecentOutboundMessages();
|
|
426
|
+
return recentOutboundMessagesById.has(key);
|
|
427
|
+
}
|
|
428
|
+
function pruneRecentWatchNotifications(now = Date.now()) {
|
|
429
|
+
for (const [key, expiresAt] of recentWatchNotificationsByMessage.entries()) {
|
|
430
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
431
|
+
recentWatchNotificationsByMessage.delete(key);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function markRecentWatchNotification(messageId, accountId) {
|
|
436
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
437
|
+
if (!key) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
pruneRecentWatchNotifications();
|
|
441
|
+
recentWatchNotificationsByMessage.set(key, Date.now() + RECENT_WATCH_NOTIFICATION_TTL_MS);
|
|
442
|
+
}
|
|
443
|
+
function hasRecentWatchNotification(messageId, accountId) {
|
|
444
|
+
const key = buildAccountMessageScopeKey(accountId, messageId);
|
|
445
|
+
if (!key) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
pruneRecentWatchNotifications();
|
|
449
|
+
return recentWatchNotificationsByMessage.has(key);
|
|
450
|
+
}
|
|
451
|
+
function buildRecentMediaDeliveryKey(dialogId, currentMessageId, accountId) {
|
|
452
|
+
return buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
453
|
+
}
|
|
454
|
+
function normalizeMediaDeliveryName(mediaUrl) {
|
|
455
|
+
return basename(mediaUrl.trim()).toLowerCase();
|
|
456
|
+
}
|
|
457
|
+
function pruneRecentMediaDeliveries(now = Date.now()) {
|
|
458
|
+
for (const [key, entry] of recentSuccessfulMediaDeliveries.entries()) {
|
|
459
|
+
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
460
|
+
recentSuccessfulMediaDeliveries.delete(key);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function markRecentMediaDelivery(params) {
|
|
465
|
+
const key = buildRecentMediaDeliveryKey(params.dialogId, params.currentMessageId, params.accountId);
|
|
466
|
+
if (!key || params.mediaUrls.length === 0) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
pruneRecentMediaDeliveries();
|
|
470
|
+
const mediaByName = recentSuccessfulMediaDeliveries.get(key)?.mediaByName ?? new Map();
|
|
471
|
+
const normalizedMessageId = String(params.messageId ?? '').trim();
|
|
472
|
+
for (const mediaUrl of params.mediaUrls) {
|
|
473
|
+
const mediaName = normalizeMediaDeliveryName(mediaUrl);
|
|
474
|
+
if (!mediaName) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
mediaByName.set(mediaName, normalizedMessageId);
|
|
478
|
+
}
|
|
479
|
+
recentSuccessfulMediaDeliveries.set(key, {
|
|
480
|
+
expiresAt: Date.now() + RECENT_MEDIA_DELIVERY_TTL_MS,
|
|
481
|
+
mediaByName,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function findRecentMediaDelivery(params) {
|
|
485
|
+
const key = buildRecentMediaDeliveryKey(params.dialogId, params.currentMessageId, params.accountId);
|
|
486
|
+
if (!key || params.mediaUrls.length === 0) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
pruneRecentMediaDeliveries();
|
|
490
|
+
const entry = recentSuccessfulMediaDeliveries.get(key);
|
|
491
|
+
if (!entry) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const normalizedNames = params.mediaUrls
|
|
495
|
+
.map((mediaUrl) => normalizeMediaDeliveryName(mediaUrl))
|
|
496
|
+
.filter(Boolean);
|
|
497
|
+
if (normalizedNames.length === 0) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
for (const mediaName of normalizedNames) {
|
|
501
|
+
if (!entry.mediaByName.has(mediaName)) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const lastMessageId = normalizedNames
|
|
506
|
+
.map((mediaName) => entry.mediaByName.get(mediaName) ?? '')
|
|
507
|
+
.find(Boolean);
|
|
508
|
+
return lastMessageId ? { messageId: lastMessageId } : {};
|
|
509
|
+
}
|
|
510
|
+
function hasRecentMediaDelivery(dialogId, currentMessageId, accountId) {
|
|
511
|
+
const key = buildRecentMediaDeliveryKey(dialogId, currentMessageId, accountId);
|
|
512
|
+
if (!key) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
pruneRecentMediaDeliveries();
|
|
516
|
+
const entry = recentSuccessfulMediaDeliveries.get(key);
|
|
517
|
+
return Boolean(entry && entry.mediaByName.size > 0);
|
|
518
|
+
}
|
|
519
|
+
function prunePendingNativeReactionAcks(now = Date.now()) {
|
|
520
|
+
for (const [key, entry] of pendingNativeReactionAcks.entries()) {
|
|
521
|
+
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
522
|
+
pendingNativeReactionAcks.delete(key);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function markPendingNativeReactionAck(currentMessageId, emoji, accountId) {
|
|
527
|
+
const key = buildPendingNativeReactionAckKey(currentMessageId, accountId);
|
|
528
|
+
const normalizedEmoji = emoji.trim();
|
|
529
|
+
if (!key || !normalizedEmoji) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
prunePendingNativeReactionAcks();
|
|
533
|
+
pendingNativeReactionAcks.set(key, {
|
|
534
|
+
emoji: normalizedEmoji,
|
|
535
|
+
expiresAt: Date.now() + NATIVE_REACTION_ACK_TTL_MS,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function consumePendingNativeReactionAck(currentMessageId, text, accountId) {
|
|
539
|
+
const key = buildPendingNativeReactionAckKey(currentMessageId, accountId);
|
|
540
|
+
if (!key) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
prunePendingNativeReactionAcks();
|
|
544
|
+
const pendingAck = pendingNativeReactionAcks.get(key);
|
|
545
|
+
if (!pendingAck) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
if (normalizeComparableMessageText(text) !== pendingAck.emoji) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
pendingNativeReactionAcks.delete(key);
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
function pruneRecentInboundMessages(now = Date.now()) {
|
|
555
|
+
for (const [dialogKey, entries] of recentInboundMessagesByDialog.entries()) {
|
|
556
|
+
const nextEntries = entries.filter((entry) => Number.isFinite(entry.timestamp) && entry.timestamp > now - RECENT_INBOUND_MESSAGE_TTL_MS);
|
|
557
|
+
if (nextEntries.length === 0) {
|
|
558
|
+
recentInboundMessagesByDialog.delete(dialogKey);
|
|
559
|
+
}
|
|
560
|
+
else if (nextEntries.length !== entries.length) {
|
|
561
|
+
recentInboundMessagesByDialog.set(dialogKey, nextEntries);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
for (const [messageId, entry] of inboundMessageContextById.entries()) {
|
|
565
|
+
if (!Number.isFinite(entry.timestamp) || entry.timestamp <= now - RECENT_INBOUND_MESSAGE_TTL_MS) {
|
|
566
|
+
inboundMessageContextById.delete(messageId);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function rememberRecentInboundMessage(dialogId, messageId, body, mediaNames = [], timestamp = Date.now(), accountId) {
|
|
571
|
+
const normalizedMessageId = toMessageId(messageId);
|
|
572
|
+
const normalizedBody = body.trim();
|
|
573
|
+
const dialogKey = buildAccountDialogScopeKey(accountId, dialogId);
|
|
574
|
+
const messageKey = buildAccountMessageScopeKey(accountId, normalizedMessageId);
|
|
575
|
+
if (!dialogKey || !messageKey || !normalizedBody) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
pruneRecentInboundMessages(timestamp);
|
|
579
|
+
const id = String(normalizedMessageId);
|
|
580
|
+
const currentEntries = recentInboundMessagesByDialog.get(dialogKey) ?? [];
|
|
581
|
+
const nextEntries = currentEntries
|
|
582
|
+
.filter((entry) => entry.messageId !== id)
|
|
583
|
+
.concat({ messageId: id, body: normalizedBody, timestamp })
|
|
584
|
+
.slice(-RECENT_INBOUND_MESSAGE_LIMIT);
|
|
585
|
+
recentInboundMessagesByDialog.set(dialogKey, nextEntries);
|
|
586
|
+
inboundMessageContextById.set(messageKey, {
|
|
587
|
+
dialogId,
|
|
588
|
+
dialogKey,
|
|
589
|
+
body: normalizedBody,
|
|
590
|
+
mediaNames: mediaNames
|
|
591
|
+
.filter((name) => typeof name === 'string')
|
|
592
|
+
.map((name) => name.trim())
|
|
593
|
+
.filter(Boolean),
|
|
594
|
+
timestamp,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
function findPreviousRecentInboundMessage(currentMessageId, accountId) {
|
|
598
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
599
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
600
|
+
if (!messageKey || !normalizedMessageId) {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
pruneRecentInboundMessages();
|
|
604
|
+
const currentEntry = inboundMessageContextById.get(messageKey);
|
|
605
|
+
if (!currentEntry) {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
const dialogEntries = recentInboundMessagesByDialog.get(currentEntry.dialogKey);
|
|
609
|
+
if (!dialogEntries || dialogEntries.length === 0) {
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
const currentIndex = dialogEntries.findIndex((entry) => entry.messageId === String(normalizedMessageId));
|
|
613
|
+
if (currentIndex <= 0) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
for (let index = currentIndex - 1; index >= 0; index -= 1) {
|
|
617
|
+
const entry = dialogEntries[index];
|
|
618
|
+
if (entry.body.trim()) {
|
|
619
|
+
return entry;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
const INLINE_REPLY_DIRECTIVE_RE = /\[\[\s*(reply_to_current|reply_to\s*:\s*([^\]\n]+))\s*\]\]/gi;
|
|
625
|
+
function normalizeComparableMessageText(value) {
|
|
626
|
+
return value
|
|
627
|
+
.replace(/\s+/g, ' ')
|
|
628
|
+
.trim();
|
|
629
|
+
}
|
|
630
|
+
function normalizeBitrix24DialogTarget(raw) {
|
|
631
|
+
const stripped = raw.trim().replace(CHANNEL_PREFIX_RE, '');
|
|
632
|
+
if (!stripped) {
|
|
633
|
+
return '';
|
|
634
|
+
}
|
|
635
|
+
const bracketedUserMatch = stripped.match(/^\[USER=(\d+)(?:[^\]]*)\][\s\S]*?\[\/USER\]$/iu);
|
|
636
|
+
if (bracketedUserMatch?.[1]) {
|
|
637
|
+
return bracketedUserMatch[1];
|
|
638
|
+
}
|
|
639
|
+
const bracketedChatMatch = stripped.match(/^\[CHAT=(?:chat)?(\d+)(?:[^\]]*)\][\s\S]*?\[\/CHAT\]$/iu);
|
|
640
|
+
if (bracketedChatMatch?.[1]) {
|
|
641
|
+
return `chat${bracketedChatMatch[1]}`;
|
|
642
|
+
}
|
|
643
|
+
const plainUserMatch = stripped.match(/^USER=(\d+)$/iu);
|
|
644
|
+
if (plainUserMatch?.[1]) {
|
|
645
|
+
return plainUserMatch[1];
|
|
646
|
+
}
|
|
647
|
+
const plainChatMatch = stripped.match(/^CHAT=(?:chat)?(\d+)$/iu);
|
|
648
|
+
if (plainChatMatch?.[1]) {
|
|
649
|
+
return `chat${plainChatMatch[1]}`;
|
|
650
|
+
}
|
|
651
|
+
const prefixedChatMatch = stripped.match(/^chat(\d+)$/iu);
|
|
652
|
+
if (prefixedChatMatch?.[1]) {
|
|
653
|
+
return `chat${prefixedChatMatch[1]}`;
|
|
654
|
+
}
|
|
655
|
+
return stripped;
|
|
656
|
+
}
|
|
657
|
+
function looksLikeBitrix24DialogTarget(raw) {
|
|
658
|
+
const normalized = normalizeBitrix24DialogTarget(raw);
|
|
659
|
+
return /^\d+$/.test(normalized) || /^chat\d+$/iu.test(normalized);
|
|
660
|
+
}
|
|
661
|
+
function extractBitrix24DialogTargetsFromText(text) {
|
|
662
|
+
const targets = new Set();
|
|
663
|
+
for (const match of text.matchAll(/\[CHAT=(?:chat)?(\d+)(?:[^\]]*)\][\s\S]*?\[\/CHAT\]/giu)) {
|
|
664
|
+
if (match[1]) {
|
|
665
|
+
targets.add(`chat${match[1]}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
for (const match of text.matchAll(/\[USER=(\d+)(?:[^\]]*)\][\s\S]*?\[\/USER\]/giu)) {
|
|
669
|
+
if (match[1]) {
|
|
670
|
+
targets.add(match[1]);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (const match of text.matchAll(/\bCHAT=(?:chat)?(\d+)\b/giu)) {
|
|
674
|
+
if (match[1]) {
|
|
675
|
+
targets.add(`chat${match[1]}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
for (const match of text.matchAll(/\bUSER=(\d+)\b/giu)) {
|
|
679
|
+
if (match[1]) {
|
|
680
|
+
targets.add(match[1]);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return [...targets];
|
|
684
|
+
}
|
|
685
|
+
function isLikelyForwardControlMessage(text) {
|
|
686
|
+
return extractBitrix24DialogTargetsFromText(text).length > 0;
|
|
687
|
+
}
|
|
688
|
+
function extractReplyDirective(params) {
|
|
689
|
+
let wantsCurrentReply = false;
|
|
690
|
+
let inlineReplyToId;
|
|
691
|
+
const cleanText = cleanupTextAfterInlineMediaExtraction(params.text.replace(INLINE_REPLY_DIRECTIVE_RE, (_match, _rawDirective, explicitId) => {
|
|
692
|
+
if (typeof explicitId === 'string' && explicitId.trim()) {
|
|
693
|
+
inlineReplyToId = explicitId.trim();
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
wantsCurrentReply = true;
|
|
697
|
+
}
|
|
698
|
+
return ' ';
|
|
699
|
+
}));
|
|
700
|
+
const explicitReplyToMessageId = parseActionMessageIds(params.explicitReplyToId)[0];
|
|
701
|
+
if (explicitReplyToMessageId) {
|
|
702
|
+
return { cleanText, replyToMessageId: explicitReplyToMessageId };
|
|
703
|
+
}
|
|
704
|
+
const inlineReplyToMessageId = parseActionMessageIds(inlineReplyToId)[0];
|
|
705
|
+
if (inlineReplyToMessageId) {
|
|
706
|
+
return { cleanText, replyToMessageId: inlineReplyToMessageId };
|
|
707
|
+
}
|
|
708
|
+
if (wantsCurrentReply) {
|
|
709
|
+
const currentReplyToMessageId = parseActionMessageIds(params.currentMessageId)[0];
|
|
710
|
+
if (currentReplyToMessageId) {
|
|
711
|
+
return { cleanText, replyToMessageId: currentReplyToMessageId };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return { cleanText };
|
|
715
|
+
}
|
|
716
|
+
function isBitrix24DirectDialogId(dialogId) {
|
|
717
|
+
const normalizedDialogId = String(dialogId ?? '').trim();
|
|
718
|
+
return normalizedDialogId.length > 0 && !normalizedDialogId.startsWith('chat');
|
|
719
|
+
}
|
|
720
|
+
function shouldUseBitrix24NativeReply(params) {
|
|
721
|
+
if (typeof params.isDm === 'boolean') {
|
|
722
|
+
return !params.isDm;
|
|
723
|
+
}
|
|
724
|
+
if (!params.dialogId) {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
return !isBitrix24DirectDialogId(params.dialogId);
|
|
728
|
+
}
|
|
729
|
+
function findNativeForwardTarget(params) {
|
|
730
|
+
const deliveredText = normalizeComparableMessageText(params.deliveredText);
|
|
731
|
+
if (!deliveredText) {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
if (deliveredText === normalizeComparableMessageText(params.requestText)) {
|
|
735
|
+
return undefined;
|
|
736
|
+
}
|
|
737
|
+
const currentMessageId = toMessageId(params.currentMessageId);
|
|
738
|
+
const matchedEntry = [...params.historyEntries]
|
|
739
|
+
.reverse()
|
|
740
|
+
.find((entry) => {
|
|
741
|
+
if (normalizeComparableMessageText(entry.body) !== deliveredText) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
return currentMessageId === undefined || toMessageId(entry.messageId) !== currentMessageId;
|
|
745
|
+
});
|
|
746
|
+
if (!matchedEntry) {
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
return toMessageId(matchedEntry.messageId);
|
|
750
|
+
}
|
|
751
|
+
function findImplicitForwardTargetFromRecentInbound(params) {
|
|
752
|
+
const normalizedDeliveredText = normalizeComparableMessageText(params.deliveredText);
|
|
753
|
+
const normalizedRequestText = normalizeComparableMessageText(params.requestText);
|
|
754
|
+
const targetDialogIds = extractBitrix24DialogTargetsFromText(params.requestText);
|
|
755
|
+
const isForwardMarkerOnly = Boolean(normalizedDeliveredText) && FORWARD_MARKER_RE.test(normalizedDeliveredText);
|
|
756
|
+
if (!normalizedDeliveredText
|
|
757
|
+
|| (!isForwardMarkerOnly
|
|
758
|
+
&& (targetDialogIds.length === 0
|
|
759
|
+
|| normalizedDeliveredText !== normalizedRequestText))) {
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(params.currentMessageId, params.accountId);
|
|
763
|
+
if (previousInboundMessage) {
|
|
764
|
+
return Number(previousInboundMessage.messageId);
|
|
765
|
+
}
|
|
766
|
+
const previousHistoryEntry = [...(params.historyEntries ?? [])]
|
|
767
|
+
.reverse()
|
|
768
|
+
.find((entry) => toMessageId(entry.messageId));
|
|
769
|
+
return toMessageId(previousHistoryEntry?.messageId);
|
|
770
|
+
}
|
|
771
|
+
function findPreviousForwardableMessageId(params) {
|
|
772
|
+
// If the user replied to a specific message, that's the source to forward
|
|
773
|
+
const replyToId = toMessageId(params.replyToMessageId);
|
|
774
|
+
if (replyToId) {
|
|
775
|
+
return replyToId;
|
|
776
|
+
}
|
|
777
|
+
const normalizedCurrentBody = normalizeComparableMessageText(params.currentBody);
|
|
778
|
+
const previousHistoryEntry = [...(params.historyEntries ?? [])]
|
|
779
|
+
.reverse()
|
|
780
|
+
.find((entry) => {
|
|
781
|
+
const messageId = toMessageId(entry.messageId);
|
|
782
|
+
const normalizedBody = normalizeComparableMessageText(entry.body);
|
|
783
|
+
if (!messageId || !normalizedBody) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
if (normalizedCurrentBody && normalizedBody === normalizedCurrentBody) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
return !isLikelyForwardControlMessage(entry.body);
|
|
790
|
+
});
|
|
791
|
+
if (previousHistoryEntry) {
|
|
792
|
+
return toMessageId(previousHistoryEntry.messageId);
|
|
793
|
+
}
|
|
794
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(params.currentMessageId, params.accountId);
|
|
795
|
+
if (previousInboundMessage) {
|
|
796
|
+
if (normalizeComparableMessageText(previousInboundMessage.body) !== normalizedCurrentBody
|
|
797
|
+
&& !isLikelyForwardControlMessage(previousInboundMessage.body)) {
|
|
798
|
+
return Number(previousInboundMessage.messageId);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
function buildBitrix24NativeForwardAgentHint(params) {
|
|
804
|
+
const targetDialogIds = extractBitrix24DialogTargetsFromText(params.currentBody);
|
|
805
|
+
const sourceMessageId = findPreviousForwardableMessageId(params);
|
|
806
|
+
if (!sourceMessageId) {
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
809
|
+
const formattedTargets = targetDialogIds
|
|
810
|
+
.map((dialogId) => (dialogId.startsWith('chat') ? `${dialogId} (chat)` : `${dialogId} (user)`))
|
|
811
|
+
.join(', ');
|
|
812
|
+
const lines = [
|
|
813
|
+
'[Bitrix24 native forward instruction]',
|
|
814
|
+
'Use this only if you intentionally want a native Bitrix24 forward instead of repeating visible text.',
|
|
815
|
+
`Previous Bitrix24 message id available for forwarding: ${sourceMessageId}.`,
|
|
816
|
+
'Use action "send" with "forwardMessageIds" set to that source message id.',
|
|
817
|
+
'If the tool requires a non-empty message field for a pure forward, use "↩️".',
|
|
818
|
+
'Do not repeat the source message text or the user instruction text when doing a native forward.',
|
|
819
|
+
];
|
|
820
|
+
if (formattedTargets) {
|
|
821
|
+
lines.push(`Explicit target dialog ids mentioned in the current message: ${formattedTargets}.`);
|
|
822
|
+
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.');
|
|
823
|
+
lines.push('Do not use CHAT=xxx or USER=xxx format for "to" — use "chat520" or "77" directly.');
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
lines.push('If you forward in the current dialog, do not set "to" to another dialog id.');
|
|
827
|
+
}
|
|
828
|
+
lines.push('[/Bitrix24 native forward instruction]');
|
|
829
|
+
return lines.join('\n');
|
|
830
|
+
}
|
|
831
|
+
function buildBitrix24NativeReplyAgentHint(currentMessageId) {
|
|
832
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
833
|
+
if (!normalizedMessageId) {
|
|
834
|
+
return undefined;
|
|
835
|
+
}
|
|
836
|
+
return [
|
|
837
|
+
'[Bitrix24 native reply instruction]',
|
|
838
|
+
`Current Bitrix24 message id: ${normalizedMessageId}.`,
|
|
839
|
+
'Only in group chats, if you intentionally want a native Bitrix24 reply to the current inbound message, set "replyToMessageId" to that id.',
|
|
840
|
+
'For text-only payloads in group chats, you can also prepend [[reply_to_current]] to the reply text.',
|
|
841
|
+
'Do not use a native reply unless you actually want reply threading.',
|
|
842
|
+
'[/Bitrix24 native reply instruction]',
|
|
843
|
+
].join('\n');
|
|
844
|
+
}
|
|
845
|
+
function buildBitrix24FileDeliveryAgentHint() {
|
|
846
|
+
return [
|
|
847
|
+
'[Bitrix24 file delivery instruction]',
|
|
848
|
+
'When you need to deliver a real file or document to the user, prefer a structured reply payload with field "mediaUrl" or "mediaUrls".',
|
|
849
|
+
'Set "mediaUrl" to the local path of the generated file in the OpenClaw workspace or managed media directory.',
|
|
850
|
+
'If the current prompt already includes a <file ...>...</file> block with the file contents, use that inline content directly and do not call read on the attached Bitrix24 media path unless you truly need to.',
|
|
851
|
+
'If read on an attached Bitrix24 media path fails, fall back to the inline <file> content from the current prompt instead of giving up.',
|
|
852
|
+
'If you use the generic message tool instead of a structured reply payload, attach the local file with "media", "filePath", or "path". Do not use "mediaUrl" with the message tool.',
|
|
853
|
+
'Use "text" only for an optional short caption.',
|
|
854
|
+
'For file requests, follow this order exactly: read/prepare content, write the output file, send that file once, then stop.',
|
|
855
|
+
'Never call the file-delivery tool before the write tool has succeeded for that file path.',
|
|
856
|
+
'If the upload tool says the media upload failed, fix the file path or create the file first. Do not blindly retry the same send.',
|
|
857
|
+
'After one successful file upload in a turn, do not send the same file again and do not send an extra text-only confirmation.',
|
|
858
|
+
'Do not output "NO_REPLY" unless you already called the message tool or emitted a structured reply payload with mediaUrl/mediaUrls in this same turn.',
|
|
859
|
+
'Do not output "NO_REPLY" after only read/write tools. NO_REPLY is allowed only after the actual file-delivery tool result succeeded in this same turn.',
|
|
860
|
+
'If the user asked for a file and you have not emitted the actual file delivery payload yet, do not answer with "NO_REPLY".',
|
|
861
|
+
'Do not place local file paths inside markdown links or plain text, because that only sends text to the user and does not upload the file.',
|
|
862
|
+
'[/Bitrix24 file delivery instruction]',
|
|
863
|
+
].join('\n');
|
|
864
|
+
}
|
|
865
|
+
export function buildBitrix24InlineButtonsAgentHint() {
|
|
866
|
+
return [
|
|
867
|
+
'[Bitrix24 buttons instruction]',
|
|
868
|
+
'For an ordinary reply in the current Bitrix24 dialog that needs buttons or multiple choices, prefer normal assistant text with inline button markup instead of calling the message tool.',
|
|
869
|
+
'Append the buttons to the end of the visible reply text using the exact format [[{"text":"Option 1","callback_data":"one"},{"text":"Option 2","callback_data":"two"}]].',
|
|
870
|
+
'The channel will strip that markup from the visible text and render native Bitrix24 buttons automatically.',
|
|
871
|
+
'Use the message tool for Bitrix24 only when you truly need an explicit action such as send to another dialog, native forward/reply, reaction, edit, delete, or file upload.',
|
|
872
|
+
'[/Bitrix24 buttons instruction]',
|
|
873
|
+
].join('\n');
|
|
874
|
+
}
|
|
875
|
+
function getDispatchCount(counts, key) {
|
|
876
|
+
const value = counts?.[key];
|
|
877
|
+
return Number.isFinite(value) ? Number(value) : 0;
|
|
878
|
+
}
|
|
879
|
+
function isEmptyDispatchResult(dispatchResult) {
|
|
880
|
+
if (!dispatchResult || dispatchResult.queuedFinal !== false) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
const toolCount = getDispatchCount(dispatchResult.counts, 'tool');
|
|
884
|
+
const blockCount = getDispatchCount(dispatchResult.counts, 'block');
|
|
885
|
+
const finalCount = getDispatchCount(dispatchResult.counts, 'final');
|
|
886
|
+
return toolCount + blockCount + finalCount === 0;
|
|
887
|
+
}
|
|
888
|
+
function readExplicitForwardMessageIds(rawParams) {
|
|
889
|
+
return parseActionMessageIds(rawParams.forwardMessageIds
|
|
890
|
+
?? rawParams.forwardIds);
|
|
891
|
+
}
|
|
892
|
+
function resolveActionForwardMessageIds(params) {
|
|
893
|
+
const explicitForwardMessages = readExplicitForwardMessageIds(params.rawParams);
|
|
894
|
+
if (explicitForwardMessages.length > 0) {
|
|
895
|
+
return explicitForwardMessages;
|
|
896
|
+
}
|
|
897
|
+
const aliasedForwardMessages = parseActionMessageIds(params.rawParams.messageIds
|
|
898
|
+
?? params.rawParams.message_ids
|
|
899
|
+
?? params.rawParams.messageId
|
|
900
|
+
?? params.rawParams.message_id
|
|
901
|
+
?? params.rawParams.replyToMessageId
|
|
902
|
+
?? params.rawParams.replyToId);
|
|
903
|
+
if (aliasedForwardMessages.length > 0) {
|
|
904
|
+
return aliasedForwardMessages;
|
|
905
|
+
}
|
|
906
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(params.toolContext?.currentMessageId, params.accountId);
|
|
907
|
+
return previousInboundMessage ? [Number(previousInboundMessage.messageId)] : [];
|
|
908
|
+
}
|
|
909
|
+
const FORWARD_MARKER_RE = /^[\s↩️➡️→⤵️🔄📨]+$/u;
|
|
910
|
+
function normalizeForwardActionText(text) {
|
|
911
|
+
const normalizedText = typeof text === 'string' ? text.trim() : '';
|
|
912
|
+
if (!normalizedText || FORWARD_MARKER_RE.test(normalizedText)) {
|
|
913
|
+
return '';
|
|
914
|
+
}
|
|
915
|
+
return normalizedText;
|
|
916
|
+
}
|
|
112
917
|
function escapeBbCodeText(value) {
|
|
113
918
|
return value
|
|
114
919
|
.replace(/\[/g, '(')
|
|
@@ -262,6 +1067,9 @@ function createReplyStatusHeartbeat(params) {
|
|
|
262
1067
|
async function waitReplyFallbackGraceWindow() {
|
|
263
1068
|
await new Promise((resolve) => setTimeout(resolve, REPLY_FALLBACK_GRACE_MS));
|
|
264
1069
|
}
|
|
1070
|
+
async function waitEmptyReplyFallbackGraceWindow() {
|
|
1071
|
+
await new Promise((resolve) => setTimeout(resolve, EMPTY_REPLY_FALLBACK_GRACE_MS));
|
|
1072
|
+
}
|
|
265
1073
|
export function canCoalesceDirectMessage(msgCtx, config) {
|
|
266
1074
|
return msgCtx.isDm
|
|
267
1075
|
&& config.dmPolicy !== 'pairing'
|
|
@@ -741,6 +1549,19 @@ class BufferedDirectMessageCoalescer {
|
|
|
741
1549
|
let gatewayState = null;
|
|
742
1550
|
export function __setGatewayStateForTests(state) {
|
|
743
1551
|
gatewayState = state;
|
|
1552
|
+
if (state === null) {
|
|
1553
|
+
pendingNativeForwardAcks.clear();
|
|
1554
|
+
pendingNativeReactionAcks.clear();
|
|
1555
|
+
recentSuccessfulMediaDeliveries.clear();
|
|
1556
|
+
recentSuccessfulActionsByMessage.clear();
|
|
1557
|
+
recentOutboundMessagesById.clear();
|
|
1558
|
+
recentWatchNotificationsByMessage.clear();
|
|
1559
|
+
recentInboundMessagesByDialog.clear();
|
|
1560
|
+
inboundMessageContextById.clear();
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
export function __rememberRecentInboundMessageForTests(params) {
|
|
1564
|
+
rememberRecentInboundMessage(params.dialogId, params.messageId, params.body, params.mediaNames, params.timestamp, params.accountId);
|
|
744
1565
|
}
|
|
745
1566
|
// ─── Keyboard layouts ────────────────────────────────────────────────────────
|
|
746
1567
|
export function buildWelcomeKeyboard(language) {
|
|
@@ -768,6 +1589,638 @@ export function buildDefaultCommandKeyboard(language) {
|
|
|
768
1589
|
}
|
|
769
1590
|
export const DEFAULT_COMMAND_KEYBOARD = buildDefaultCommandKeyboard();
|
|
770
1591
|
export const DEFAULT_WELCOME_KEYBOARD = buildWelcomeKeyboard();
|
|
1592
|
+
const BITRIX24_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'delete'];
|
|
1593
|
+
const BITRIX24_DISCOVERY_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'delete'];
|
|
1594
|
+
function buildMessageToolButtonsSchema() {
|
|
1595
|
+
return {
|
|
1596
|
+
type: 'array',
|
|
1597
|
+
description: 'Optional Bitrix24 message buttons as rows of button objects. Each button object can include text, optional callback_data and optional style.',
|
|
1598
|
+
items: {
|
|
1599
|
+
type: 'array',
|
|
1600
|
+
items: {
|
|
1601
|
+
type: 'object',
|
|
1602
|
+
description: 'Single button object. Use text, optional callback_data, and optional style=primary|attention|danger.',
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function buildMessageToolAttachSchema() {
|
|
1608
|
+
return {
|
|
1609
|
+
description: 'Bitrix24 ATTACH rich layout blocks. Use this for rich cards, not for binary file uploads. Accepts either an array of block objects or an object with BLOCKS array. Prefer direct Bitrix block keys such as [{USER:{USER_ID:1,NAME:"Eugene"}},{GRID:[{DISPLAY:"LINE",NAME:"Status",VALUE:"In progress"},{DISPLAY:"LINE",NAME:"Owner",VALUE:"Eugene"}]},{LINK:{NAME:"Open",LINK:"https://example.com"}}]. Supported top-level block keys include MESSAGE, LINK, IMAGE, FILE, DELIMITER, GRID and USER. For actual file uploads, use the generic message tool fields media/filePath/path or a structured reply payload with mediaUrl/mediaUrls.',
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function buildMessageToolIdListSchema(description) {
|
|
1613
|
+
return {
|
|
1614
|
+
description: `${description} Can be an integer, a string, or an array of integers.`,
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function resolveConfiguredBitrix24ActionAccounts(cfg, accountId) {
|
|
1618
|
+
if (accountId) {
|
|
1619
|
+
const account = resolveAccount(cfg, accountId);
|
|
1620
|
+
return account.enabled && account.configured ? [account.accountId] : [];
|
|
1621
|
+
}
|
|
1622
|
+
return listAccountIds(cfg).filter((candidateId) => {
|
|
1623
|
+
const account = resolveAccount(cfg, candidateId);
|
|
1624
|
+
return account.enabled && account.configured;
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
function describeBitrix24MessageTool(params) {
|
|
1628
|
+
const configuredAccounts = resolveConfiguredBitrix24ActionAccounts(params.cfg, params.accountId);
|
|
1629
|
+
if (configuredAccounts.length === 0) {
|
|
1630
|
+
return {
|
|
1631
|
+
actions: [],
|
|
1632
|
+
capabilities: [],
|
|
1633
|
+
schema: null,
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
actions: [...BITRIX24_DISCOVERY_ACTION_NAMES],
|
|
1638
|
+
capabilities: ['interactive', 'buttons', 'cards'],
|
|
1639
|
+
schema: {
|
|
1640
|
+
properties: {
|
|
1641
|
+
buttons: buildMessageToolButtonsSchema(),
|
|
1642
|
+
attach: buildMessageToolAttachSchema(),
|
|
1643
|
+
forwardMessageIds: buildMessageToolIdListSchema('Bitrix24 source message id(s) to forward natively. Use with action="send" plus to/target. Prefer the referenced message id, not the current instruction message id. The message field must be non-empty — use "↩️" (emoji) for a pure forward without extra text.'),
|
|
1644
|
+
forwardIds: buildMessageToolIdListSchema('Alias for forwardMessageIds. Use with action="send".'),
|
|
1645
|
+
replyToId: buildMessageToolIdListSchema('Bitrix24 message id to reply to natively inside the current dialog.'),
|
|
1646
|
+
replyToMessageId: buildMessageToolIdListSchema('Alias for replyToId.'),
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function extractBitrix24ToolSend(args) {
|
|
1652
|
+
if ((typeof args.action === 'string' ? args.action.trim() : '') !== 'sendMessage') {
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
const to = typeof args.to === 'string' ? normalizeBitrix24DialogTarget(args.to) : '';
|
|
1656
|
+
if (!to) {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
const accountId = typeof args.accountId === 'string'
|
|
1660
|
+
? args.accountId.trim()
|
|
1661
|
+
: undefined;
|
|
1662
|
+
const threadId = typeof args.threadId === 'number'
|
|
1663
|
+
? String(args.threadId)
|
|
1664
|
+
: typeof args.threadId === 'string'
|
|
1665
|
+
? args.threadId.trim()
|
|
1666
|
+
: '';
|
|
1667
|
+
return {
|
|
1668
|
+
to,
|
|
1669
|
+
...(accountId ? { accountId } : {}),
|
|
1670
|
+
...(threadId ? { threadId } : {}),
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function readActionTextParam(params) {
|
|
1674
|
+
const rawText = params.message ?? params.text ?? params.content;
|
|
1675
|
+
return typeof rawText === 'string' ? rawText : null;
|
|
1676
|
+
}
|
|
1677
|
+
function isPlainObject(value) {
|
|
1678
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1679
|
+
}
|
|
1680
|
+
function isAttachColorToken(value) {
|
|
1681
|
+
return value === 'primary'
|
|
1682
|
+
|| value === 'secondary'
|
|
1683
|
+
|| value === 'alert'
|
|
1684
|
+
|| value === 'base';
|
|
1685
|
+
}
|
|
1686
|
+
function isPresent(value) {
|
|
1687
|
+
return value != null;
|
|
1688
|
+
}
|
|
1689
|
+
function isAttachBlock(value) {
|
|
1690
|
+
return isPlainObject(value) && Object.keys(value).length > 0;
|
|
1691
|
+
}
|
|
1692
|
+
function isBitrix24Attach(value) {
|
|
1693
|
+
if (Array.isArray(value)) {
|
|
1694
|
+
return value.length > 0 && value.every(isAttachBlock);
|
|
1695
|
+
}
|
|
1696
|
+
if (!isPlainObject(value) || !Array.isArray(value.BLOCKS) || value.BLOCKS.length === 0) {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
if (value.COLOR_TOKEN !== undefined && !isAttachColorToken(value.COLOR_TOKEN)) {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
return value.BLOCKS.every(isAttachBlock);
|
|
1703
|
+
}
|
|
1704
|
+
function readAttachString(value) {
|
|
1705
|
+
if (typeof value !== 'string') {
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
const trimmed = value.trim();
|
|
1709
|
+
return trimmed ? trimmed : undefined;
|
|
1710
|
+
}
|
|
1711
|
+
function readAttachNumber(value) {
|
|
1712
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1713
|
+
return Math.trunc(value);
|
|
1714
|
+
}
|
|
1715
|
+
if (typeof value === 'string') {
|
|
1716
|
+
const trimmed = value.trim();
|
|
1717
|
+
if (!trimmed) {
|
|
1718
|
+
return undefined;
|
|
1719
|
+
}
|
|
1720
|
+
const normalized = Number(trimmed);
|
|
1721
|
+
if (Number.isFinite(normalized)) {
|
|
1722
|
+
return Math.trunc(normalized);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return undefined;
|
|
1726
|
+
}
|
|
1727
|
+
function normalizeAttachColorTokenValue(value) {
|
|
1728
|
+
if (value === undefined) {
|
|
1729
|
+
return undefined;
|
|
1730
|
+
}
|
|
1731
|
+
if (typeof value !== 'string') {
|
|
1732
|
+
return undefined;
|
|
1733
|
+
}
|
|
1734
|
+
const normalized = value.trim().toLowerCase();
|
|
1735
|
+
return isAttachColorToken(normalized) ? normalized : undefined;
|
|
1736
|
+
}
|
|
1737
|
+
function normalizeAttachGridDisplayValue(value) {
|
|
1738
|
+
if (typeof value !== 'string') {
|
|
1739
|
+
return undefined;
|
|
1740
|
+
}
|
|
1741
|
+
const normalized = value.trim().toUpperCase();
|
|
1742
|
+
return normalized === 'BLOCK'
|
|
1743
|
+
|| normalized === 'LINE'
|
|
1744
|
+
|| normalized === 'ROW'
|
|
1745
|
+
|| normalized === 'TABLE'
|
|
1746
|
+
? normalized
|
|
1747
|
+
: undefined;
|
|
1748
|
+
}
|
|
1749
|
+
function normalizeAttachGridItem(rawValue) {
|
|
1750
|
+
if (!isPlainObject(rawValue)) {
|
|
1751
|
+
return undefined;
|
|
1752
|
+
}
|
|
1753
|
+
const display = normalizeAttachGridDisplayValue(rawValue.DISPLAY) ?? 'LINE';
|
|
1754
|
+
const name = readAttachString(rawValue.NAME);
|
|
1755
|
+
const value = readAttachString(rawValue.VALUE);
|
|
1756
|
+
const width = readAttachNumber(rawValue.WIDTH);
|
|
1757
|
+
const height = readAttachNumber(rawValue.HEIGHT);
|
|
1758
|
+
const colorToken = normalizeAttachColorTokenValue(rawValue.COLOR_TOKEN);
|
|
1759
|
+
const color = readAttachString(rawValue.COLOR);
|
|
1760
|
+
const link = readAttachString(rawValue.LINK);
|
|
1761
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
1762
|
+
const chatId = readAttachNumber(rawValue.CHAT_ID);
|
|
1763
|
+
if (!name && !value && !link && userId === undefined && chatId === undefined) {
|
|
1764
|
+
return undefined;
|
|
1765
|
+
}
|
|
1766
|
+
return {
|
|
1767
|
+
DISPLAY: display,
|
|
1768
|
+
...(name ? { NAME: name } : {}),
|
|
1769
|
+
...(value ? { VALUE: value } : {}),
|
|
1770
|
+
...(width !== undefined ? { WIDTH: width } : {}),
|
|
1771
|
+
...(height !== undefined ? { HEIGHT: height } : {}),
|
|
1772
|
+
...(colorToken ? { COLOR_TOKEN: colorToken } : {}),
|
|
1773
|
+
...(color ? { COLOR: color } : {}),
|
|
1774
|
+
...(link ? { LINK: link } : {}),
|
|
1775
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
1776
|
+
...(chatId !== undefined ? { CHAT_ID: chatId } : {}),
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
function normalizeAttachLinkValue(rawValue, aliasSource) {
|
|
1780
|
+
if (typeof rawValue === 'string') {
|
|
1781
|
+
const link = readAttachString(rawValue);
|
|
1782
|
+
if (!link) {
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
const aliasName = aliasSource
|
|
1786
|
+
? readAttachString(aliasSource.NAME) ?? readAttachString(aliasSource.TITLE)
|
|
1787
|
+
: undefined;
|
|
1788
|
+
const aliasDesc = aliasSource ? readAttachString(aliasSource.DESC) : undefined;
|
|
1789
|
+
const aliasHtml = aliasSource ? readAttachString(aliasSource.HTML) : undefined;
|
|
1790
|
+
const aliasPreview = aliasSource ? readAttachString(aliasSource.PREVIEW) : undefined;
|
|
1791
|
+
const aliasWidth = aliasSource ? readAttachNumber(aliasSource.WIDTH) : undefined;
|
|
1792
|
+
const aliasHeight = aliasSource ? readAttachNumber(aliasSource.HEIGHT) : undefined;
|
|
1793
|
+
const aliasUserId = aliasSource ? readAttachNumber(aliasSource.USER_ID) : undefined;
|
|
1794
|
+
const aliasChatId = aliasSource ? readAttachNumber(aliasSource.CHAT_ID) : undefined;
|
|
1795
|
+
const aliasNetworkId = aliasSource ? readAttachString(aliasSource.NETWORK_ID) : undefined;
|
|
1796
|
+
return {
|
|
1797
|
+
LINK: link,
|
|
1798
|
+
...(aliasName ? { NAME: aliasName } : {}),
|
|
1799
|
+
...(aliasDesc ? { DESC: aliasDesc } : {}),
|
|
1800
|
+
...(aliasHtml ? { HTML: aliasHtml } : {}),
|
|
1801
|
+
...(aliasPreview ? { PREVIEW: aliasPreview } : {}),
|
|
1802
|
+
...(aliasWidth !== undefined ? { WIDTH: aliasWidth } : {}),
|
|
1803
|
+
...(aliasHeight !== undefined ? { HEIGHT: aliasHeight } : {}),
|
|
1804
|
+
...(aliasUserId !== undefined ? { USER_ID: aliasUserId } : {}),
|
|
1805
|
+
...(aliasChatId !== undefined ? { CHAT_ID: aliasChatId } : {}),
|
|
1806
|
+
...(aliasNetworkId ? { NETWORK_ID: aliasNetworkId } : {}),
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
if (!isPlainObject(rawValue)) {
|
|
1810
|
+
return undefined;
|
|
1811
|
+
}
|
|
1812
|
+
const link = readAttachString(rawValue.LINK);
|
|
1813
|
+
if (!link) {
|
|
1814
|
+
return undefined;
|
|
1815
|
+
}
|
|
1816
|
+
const name = readAttachString(rawValue.NAME) ?? readAttachString(rawValue.TITLE);
|
|
1817
|
+
const desc = readAttachString(rawValue.DESC);
|
|
1818
|
+
const html = readAttachString(rawValue.HTML);
|
|
1819
|
+
const preview = readAttachString(rawValue.PREVIEW);
|
|
1820
|
+
const width = readAttachNumber(rawValue.WIDTH);
|
|
1821
|
+
const height = readAttachNumber(rawValue.HEIGHT);
|
|
1822
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
1823
|
+
const chatId = readAttachNumber(rawValue.CHAT_ID);
|
|
1824
|
+
const networkId = readAttachString(rawValue.NETWORK_ID);
|
|
1825
|
+
return {
|
|
1826
|
+
LINK: link,
|
|
1827
|
+
...(name ? { NAME: name } : {}),
|
|
1828
|
+
...(desc ? { DESC: desc } : {}),
|
|
1829
|
+
...(html ? { HTML: html } : {}),
|
|
1830
|
+
...(preview ? { PREVIEW: preview } : {}),
|
|
1831
|
+
...(width !== undefined ? { WIDTH: width } : {}),
|
|
1832
|
+
...(height !== undefined ? { HEIGHT: height } : {}),
|
|
1833
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
1834
|
+
...(chatId !== undefined ? { CHAT_ID: chatId } : {}),
|
|
1835
|
+
...(networkId ? { NETWORK_ID: networkId } : {}),
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function normalizeAttachImageItem(rawValue) {
|
|
1839
|
+
const normalized = normalizeAttachLinkValue(rawValue);
|
|
1840
|
+
if (!normalized) {
|
|
1841
|
+
return undefined;
|
|
1842
|
+
}
|
|
1843
|
+
return {
|
|
1844
|
+
LINK: normalized.LINK,
|
|
1845
|
+
...(normalized.NAME ? { NAME: normalized.NAME } : {}),
|
|
1846
|
+
...(normalized.PREVIEW ? { PREVIEW: normalized.PREVIEW } : {}),
|
|
1847
|
+
...(normalized.WIDTH !== undefined ? { WIDTH: normalized.WIDTH } : {}),
|
|
1848
|
+
...(normalized.HEIGHT !== undefined ? { HEIGHT: normalized.HEIGHT } : {}),
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
function normalizeAttachFileItem(rawValue) {
|
|
1852
|
+
if (typeof rawValue === 'string') {
|
|
1853
|
+
const link = readAttachString(rawValue);
|
|
1854
|
+
return link ? { LINK: link } : undefined;
|
|
1855
|
+
}
|
|
1856
|
+
if (!isPlainObject(rawValue)) {
|
|
1857
|
+
return undefined;
|
|
1858
|
+
}
|
|
1859
|
+
const link = readAttachString(rawValue.LINK);
|
|
1860
|
+
if (!link) {
|
|
1861
|
+
return undefined;
|
|
1862
|
+
}
|
|
1863
|
+
const name = readAttachString(rawValue.NAME);
|
|
1864
|
+
const size = readAttachNumber(rawValue.SIZE);
|
|
1865
|
+
return {
|
|
1866
|
+
LINK: link,
|
|
1867
|
+
...(name ? { NAME: name } : {}),
|
|
1868
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function normalizeAttachUserValue(rawValue, aliasSource) {
|
|
1872
|
+
if (typeof rawValue === 'number' || typeof rawValue === 'string') {
|
|
1873
|
+
const userId = readAttachNumber(rawValue);
|
|
1874
|
+
if (userId === undefined) {
|
|
1875
|
+
return undefined;
|
|
1876
|
+
}
|
|
1877
|
+
const aliasName = aliasSource
|
|
1878
|
+
? readAttachString(aliasSource.NAME) ?? readAttachString(aliasSource.USER_NAME)
|
|
1879
|
+
: undefined;
|
|
1880
|
+
const aliasAvatar = aliasSource ? readAttachString(aliasSource.AVATAR) : undefined;
|
|
1881
|
+
const aliasLink = aliasSource ? readAttachString(aliasSource.LINK) : undefined;
|
|
1882
|
+
const aliasNetworkId = aliasSource ? readAttachString(aliasSource.NETWORK_ID) : undefined;
|
|
1883
|
+
return {
|
|
1884
|
+
USER_ID: userId,
|
|
1885
|
+
...(aliasName ? { NAME: aliasName } : {}),
|
|
1886
|
+
...(aliasAvatar ? { AVATAR: aliasAvatar } : {}),
|
|
1887
|
+
...(aliasLink ? { LINK: aliasLink } : {}),
|
|
1888
|
+
...(aliasNetworkId ? { NETWORK_ID: aliasNetworkId } : {}),
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
if (!isPlainObject(rawValue)) {
|
|
1892
|
+
return undefined;
|
|
1893
|
+
}
|
|
1894
|
+
const userId = readAttachNumber(rawValue.USER_ID);
|
|
1895
|
+
const name = readAttachString(rawValue.NAME) ?? readAttachString(rawValue.USER_NAME);
|
|
1896
|
+
const avatar = readAttachString(rawValue.AVATAR);
|
|
1897
|
+
const link = readAttachString(rawValue.LINK);
|
|
1898
|
+
const networkId = readAttachString(rawValue.NETWORK_ID);
|
|
1899
|
+
if (userId === undefined && !name && !avatar && !link && !networkId) {
|
|
1900
|
+
return undefined;
|
|
1901
|
+
}
|
|
1902
|
+
return {
|
|
1903
|
+
...(name ? { NAME: name } : {}),
|
|
1904
|
+
...(avatar ? { AVATAR: avatar } : {}),
|
|
1905
|
+
...(link ? { LINK: link } : {}),
|
|
1906
|
+
...(userId !== undefined ? { USER_ID: userId } : {}),
|
|
1907
|
+
...(networkId ? { NETWORK_ID: networkId } : {}),
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function normalizeAttachBlock(rawValue) {
|
|
1911
|
+
if (!isPlainObject(rawValue)) {
|
|
1912
|
+
return undefined;
|
|
1913
|
+
}
|
|
1914
|
+
const message = readAttachString(rawValue.MESSAGE);
|
|
1915
|
+
if (message && Object.keys(rawValue).every((key) => key === 'MESSAGE' || key === 'TYPE')) {
|
|
1916
|
+
return { MESSAGE: message };
|
|
1917
|
+
}
|
|
1918
|
+
if ('LINK' in rawValue && rawValue.LINK !== undefined) {
|
|
1919
|
+
const linkValue = normalizeAttachLinkValue(rawValue.LINK, rawValue);
|
|
1920
|
+
if (linkValue) {
|
|
1921
|
+
return { LINK: linkValue };
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
if ('IMAGE' in rawValue && rawValue.IMAGE !== undefined) {
|
|
1925
|
+
const rawImage = rawValue.IMAGE;
|
|
1926
|
+
const imageItems = Array.isArray(rawImage)
|
|
1927
|
+
? rawImage.map(normalizeAttachImageItem).filter(isPresent)
|
|
1928
|
+
: [normalizeAttachImageItem(rawImage)].filter(isPresent);
|
|
1929
|
+
if (imageItems.length > 0) {
|
|
1930
|
+
return { IMAGE: imageItems.length === 1 ? imageItems[0] : imageItems };
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if ('FILE' in rawValue && rawValue.FILE !== undefined) {
|
|
1934
|
+
const rawFile = rawValue.FILE;
|
|
1935
|
+
const fileItems = Array.isArray(rawFile)
|
|
1936
|
+
? rawFile.map(normalizeAttachFileItem).filter(isPresent)
|
|
1937
|
+
: [normalizeAttachFileItem(rawFile)].filter(isPresent);
|
|
1938
|
+
if (fileItems.length > 0) {
|
|
1939
|
+
return { FILE: fileItems.length === 1 ? fileItems[0] : fileItems };
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if ('DELIMITER' in rawValue && isPlainObject(rawValue.DELIMITER)) {
|
|
1943
|
+
const delimiter = rawValue.DELIMITER;
|
|
1944
|
+
const size = readAttachNumber(delimiter.SIZE);
|
|
1945
|
+
const color = readAttachString(delimiter.COLOR);
|
|
1946
|
+
if (size !== undefined || color) {
|
|
1947
|
+
return {
|
|
1948
|
+
DELIMITER: {
|
|
1949
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
1950
|
+
...(color ? { COLOR: color } : {}),
|
|
1951
|
+
},
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if ('GRID' in rawValue && Array.isArray(rawValue.GRID)) {
|
|
1956
|
+
const gridItems = rawValue.GRID.map(normalizeAttachGridItem).filter(isPresent);
|
|
1957
|
+
if (gridItems.length > 0) {
|
|
1958
|
+
return { GRID: gridItems };
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if ('USER' in rawValue && rawValue.USER !== undefined) {
|
|
1962
|
+
const userValue = normalizeAttachUserValue(rawValue.USER, rawValue);
|
|
1963
|
+
if (userValue) {
|
|
1964
|
+
return { USER: userValue };
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
const rawType = readAttachString(rawValue.TYPE)?.toUpperCase();
|
|
1968
|
+
if (!rawType) {
|
|
1969
|
+
return undefined;
|
|
1970
|
+
}
|
|
1971
|
+
if (rawType === 'MESSAGE') {
|
|
1972
|
+
const title = readAttachString(rawValue.TITLE);
|
|
1973
|
+
return message
|
|
1974
|
+
? { MESSAGE: message }
|
|
1975
|
+
: title
|
|
1976
|
+
? { MESSAGE: `[B]${title}[/B]` }
|
|
1977
|
+
: undefined;
|
|
1978
|
+
}
|
|
1979
|
+
if (rawType === 'TITLE') {
|
|
1980
|
+
const title = readAttachString(rawValue.TITLE) ?? message;
|
|
1981
|
+
return title ? { MESSAGE: `[B]${title}[/B]` } : undefined;
|
|
1982
|
+
}
|
|
1983
|
+
if (rawType === 'LINK') {
|
|
1984
|
+
const linkValue = normalizeAttachLinkValue(rawValue.LINK ?? rawValue, rawValue);
|
|
1985
|
+
return linkValue ? { LINK: linkValue } : undefined;
|
|
1986
|
+
}
|
|
1987
|
+
if (rawType === 'USER') {
|
|
1988
|
+
const userValue = normalizeAttachUserValue(rawValue.USER ?? rawValue.USER_ID ?? rawValue, rawValue);
|
|
1989
|
+
return userValue ? { USER: userValue } : undefined;
|
|
1990
|
+
}
|
|
1991
|
+
if (rawType === 'GRID') {
|
|
1992
|
+
const rawGridItems = Array.isArray(rawValue.GRID)
|
|
1993
|
+
? rawValue.GRID
|
|
1994
|
+
: Array.isArray(rawValue.ITEMS)
|
|
1995
|
+
? rawValue.ITEMS
|
|
1996
|
+
: Array.isArray(rawValue.ROWS)
|
|
1997
|
+
? rawValue.ROWS
|
|
1998
|
+
: [];
|
|
1999
|
+
const gridItems = rawGridItems.map(normalizeAttachGridItem).filter(isPresent);
|
|
2000
|
+
return gridItems.length > 0 ? { GRID: gridItems } : undefined;
|
|
2001
|
+
}
|
|
2002
|
+
if (rawType === 'DELIMITER') {
|
|
2003
|
+
const size = readAttachNumber(rawValue.SIZE);
|
|
2004
|
+
const color = readAttachString(rawValue.COLOR);
|
|
2005
|
+
return size !== undefined || color
|
|
2006
|
+
? {
|
|
2007
|
+
DELIMITER: {
|
|
2008
|
+
...(size !== undefined ? { SIZE: size } : {}),
|
|
2009
|
+
...(color ? { COLOR: color } : {}),
|
|
2010
|
+
},
|
|
2011
|
+
}
|
|
2012
|
+
: undefined;
|
|
2013
|
+
}
|
|
2014
|
+
if (rawType === 'IMAGE') {
|
|
2015
|
+
const rawImage = rawValue.IMAGE ?? rawValue.LINK ?? rawValue;
|
|
2016
|
+
const imageItems = Array.isArray(rawImage)
|
|
2017
|
+
? rawImage.map(normalizeAttachImageItem).filter(isPresent)
|
|
2018
|
+
: [normalizeAttachImageItem(rawImage)].filter(isPresent);
|
|
2019
|
+
return imageItems.length > 0
|
|
2020
|
+
? { IMAGE: imageItems.length === 1 ? imageItems[0] : imageItems }
|
|
2021
|
+
: undefined;
|
|
2022
|
+
}
|
|
2023
|
+
if (rawType === 'FILE') {
|
|
2024
|
+
const rawFile = rawValue.FILE ?? rawValue.LINK ?? rawValue;
|
|
2025
|
+
const fileItems = Array.isArray(rawFile)
|
|
2026
|
+
? rawFile.map(normalizeAttachFileItem).filter(isPresent)
|
|
2027
|
+
: [normalizeAttachFileItem(rawFile)].filter(isPresent);
|
|
2028
|
+
return fileItems.length > 0
|
|
2029
|
+
? { FILE: fileItems.length === 1 ? fileItems[0] : fileItems }
|
|
2030
|
+
: undefined;
|
|
2031
|
+
}
|
|
2032
|
+
return undefined;
|
|
2033
|
+
}
|
|
2034
|
+
function normalizeBitrix24Attach(rawValue) {
|
|
2035
|
+
if (Array.isArray(rawValue)) {
|
|
2036
|
+
const blocks = rawValue.map(normalizeAttachBlock).filter(isPresent);
|
|
2037
|
+
return blocks.length > 0 ? blocks : undefined;
|
|
2038
|
+
}
|
|
2039
|
+
if (!isPlainObject(rawValue)) {
|
|
2040
|
+
return undefined;
|
|
2041
|
+
}
|
|
2042
|
+
if (Array.isArray(rawValue.BLOCKS)) {
|
|
2043
|
+
const blocks = rawValue.BLOCKS.map(normalizeAttachBlock).filter(isPresent);
|
|
2044
|
+
if (blocks.length === 0) {
|
|
2045
|
+
return undefined;
|
|
2046
|
+
}
|
|
2047
|
+
const id = readAttachNumber(rawValue.ID);
|
|
2048
|
+
const colorToken = normalizeAttachColorTokenValue(rawValue.COLOR_TOKEN);
|
|
2049
|
+
const color = readAttachString(rawValue.COLOR);
|
|
2050
|
+
return {
|
|
2051
|
+
...(id !== undefined ? { ID: id } : {}),
|
|
2052
|
+
...(colorToken ? { COLOR_TOKEN: colorToken } : {}),
|
|
2053
|
+
...(color ? { COLOR: color } : {}),
|
|
2054
|
+
BLOCKS: blocks,
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
const block = normalizeAttachBlock(rawValue);
|
|
2058
|
+
return block ? [block] : undefined;
|
|
2059
|
+
}
|
|
2060
|
+
function parseActionAttach(rawValue) {
|
|
2061
|
+
if (rawValue == null) {
|
|
2062
|
+
return undefined;
|
|
2063
|
+
}
|
|
2064
|
+
let parsed = rawValue;
|
|
2065
|
+
if (typeof rawValue === 'string') {
|
|
2066
|
+
const trimmed = rawValue.trim();
|
|
2067
|
+
if (!trimmed) {
|
|
2068
|
+
return undefined;
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
parsed = JSON.parse(trimmed);
|
|
2072
|
+
}
|
|
2073
|
+
catch {
|
|
2074
|
+
return undefined;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return normalizeBitrix24Attach(parsed);
|
|
2078
|
+
}
|
|
2079
|
+
function hasMeaningfulAttachInput(rawValue) {
|
|
2080
|
+
if (rawValue == null) {
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
if (typeof rawValue === 'string') {
|
|
2084
|
+
const trimmed = rawValue.trim();
|
|
2085
|
+
if (!trimmed) {
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
try {
|
|
2089
|
+
return hasMeaningfulAttachInput(JSON.parse(trimmed));
|
|
2090
|
+
}
|
|
2091
|
+
catch {
|
|
2092
|
+
return true;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (Array.isArray(rawValue)) {
|
|
2096
|
+
return rawValue.length > 0;
|
|
2097
|
+
}
|
|
2098
|
+
if (isPlainObject(rawValue)) {
|
|
2099
|
+
if (Array.isArray(rawValue.BLOCKS)) {
|
|
2100
|
+
return rawValue.BLOCKS.length > 0;
|
|
2101
|
+
}
|
|
2102
|
+
return Object.keys(rawValue).length > 0;
|
|
2103
|
+
}
|
|
2104
|
+
return true;
|
|
2105
|
+
}
|
|
2106
|
+
function readActionTargetParam(params, fallbackTarget) {
|
|
2107
|
+
const rawTarget = params.to ?? params.target ?? fallbackTarget;
|
|
2108
|
+
return typeof rawTarget === 'string' ? normalizeBitrix24DialogTarget(rawTarget) : '';
|
|
2109
|
+
}
|
|
2110
|
+
function parseActionMessageIds(rawValue) {
|
|
2111
|
+
const ids = [];
|
|
2112
|
+
const seen = new Set();
|
|
2113
|
+
const append = (value) => {
|
|
2114
|
+
if (Array.isArray(value)) {
|
|
2115
|
+
value.forEach(append);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (typeof value === 'number') {
|
|
2119
|
+
const normalizedId = Math.trunc(value);
|
|
2120
|
+
if (Number.isFinite(normalizedId) && normalizedId > 0 && !seen.has(normalizedId)) {
|
|
2121
|
+
seen.add(normalizedId);
|
|
2122
|
+
ids.push(normalizedId);
|
|
2123
|
+
}
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (typeof value !== 'string') {
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
const trimmed = value.trim();
|
|
2130
|
+
if (!trimmed) {
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (trimmed.startsWith('[')) {
|
|
2134
|
+
try {
|
|
2135
|
+
append(JSON.parse(trimmed));
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
catch {
|
|
2139
|
+
// Fall through to scalar / CSV parsing.
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
trimmed
|
|
2143
|
+
.split(/[,\s]+/)
|
|
2144
|
+
.forEach((part) => {
|
|
2145
|
+
if (!part) {
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const normalizedId = Number(part);
|
|
2149
|
+
if (Number.isFinite(normalizedId) && normalizedId > 0 && !seen.has(normalizedId)) {
|
|
2150
|
+
seen.add(normalizedId);
|
|
2151
|
+
ids.push(normalizedId);
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
};
|
|
2155
|
+
append(rawValue);
|
|
2156
|
+
return ids;
|
|
2157
|
+
}
|
|
2158
|
+
function parseActionKeyboard(rawButtons) {
|
|
2159
|
+
if (rawButtons == null) {
|
|
2160
|
+
return undefined;
|
|
2161
|
+
}
|
|
2162
|
+
try {
|
|
2163
|
+
const parsed = typeof rawButtons === 'string' ? JSON.parse(rawButtons) : rawButtons;
|
|
2164
|
+
if (Array.isArray(parsed)) {
|
|
2165
|
+
const keyboard = convertButtonsToKeyboard(parsed);
|
|
2166
|
+
return keyboard.length > 0 ? keyboard : undefined;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
catch {
|
|
2170
|
+
// Ignore invalid buttons payload and send without keyboard.
|
|
2171
|
+
}
|
|
2172
|
+
return undefined;
|
|
2173
|
+
}
|
|
2174
|
+
function collectActionMediaUrls(params) {
|
|
2175
|
+
const mediaUrls = [];
|
|
2176
|
+
const seen = new Set();
|
|
2177
|
+
const append = (value) => {
|
|
2178
|
+
if (Array.isArray(value)) {
|
|
2179
|
+
value.forEach(append);
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
if (typeof value !== 'string') {
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const trimmed = value.trim();
|
|
2186
|
+
if (!trimmed) {
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (trimmed.startsWith('[')) {
|
|
2190
|
+
try {
|
|
2191
|
+
append(JSON.parse(trimmed));
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
catch {
|
|
2195
|
+
// Fall through to scalar handling.
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (!seen.has(trimmed)) {
|
|
2199
|
+
seen.add(trimmed);
|
|
2200
|
+
mediaUrls.push(trimmed);
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
append(params.mediaUrl);
|
|
2204
|
+
append(params.mediaUrls);
|
|
2205
|
+
append(params.media);
|
|
2206
|
+
append(params.filePath);
|
|
2207
|
+
append(params.filePaths);
|
|
2208
|
+
append(params.path);
|
|
2209
|
+
append(params.paths);
|
|
2210
|
+
return mediaUrls;
|
|
2211
|
+
}
|
|
2212
|
+
function buildActionMessageText(params) {
|
|
2213
|
+
const normalizedText = typeof params.text === 'string'
|
|
2214
|
+
? markdownToBbCode(params.text.trim())
|
|
2215
|
+
: '';
|
|
2216
|
+
if (normalizedText) {
|
|
2217
|
+
return normalizedText;
|
|
2218
|
+
}
|
|
2219
|
+
if (params.attach) {
|
|
2220
|
+
return null;
|
|
2221
|
+
}
|
|
2222
|
+
return params.keyboard ? ' ' : null;
|
|
2223
|
+
}
|
|
771
2224
|
function parseRegisteredCommandTrigger(callbackData) {
|
|
772
2225
|
const trimmed = callbackData.trim();
|
|
773
2226
|
const isSlashCommand = trimmed.startsWith('/');
|
|
@@ -844,6 +2297,13 @@ export function extractKeyboardFromPayload(payload) {
|
|
|
844
2297
|
}
|
|
845
2298
|
return undefined;
|
|
846
2299
|
}
|
|
2300
|
+
export function extractAttachFromPayload(payload) {
|
|
2301
|
+
const cd = payload.channelData;
|
|
2302
|
+
if (!cd)
|
|
2303
|
+
return undefined;
|
|
2304
|
+
const b24Data = cd.bitrix24;
|
|
2305
|
+
return parseActionAttach(b24Data?.attach ?? b24Data?.attachments);
|
|
2306
|
+
}
|
|
847
2307
|
/**
|
|
848
2308
|
* Extract inline button JSON embedded in message text by the agent.
|
|
849
2309
|
*
|
|
@@ -874,6 +2334,13 @@ export function extractInlineButtonsFromText(text) {
|
|
|
874
2334
|
return undefined;
|
|
875
2335
|
}
|
|
876
2336
|
}
|
|
2337
|
+
function cleanupTextAfterInlineMediaExtraction(text) {
|
|
2338
|
+
return text
|
|
2339
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
2340
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
2341
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
2342
|
+
.trim();
|
|
2343
|
+
}
|
|
877
2344
|
function normalizeCommandReplyPayload(params) {
|
|
878
2345
|
const { commandName, commandParams, text, language } = params;
|
|
879
2346
|
if (commandName === 'models' && commandParams.trim() === '') {
|
|
@@ -1130,12 +2597,13 @@ export async function handleWebhookRequest(req, res) {
|
|
|
1130
2597
|
// ─── Outbound adapter helpers ────────────────────────────────────────────────
|
|
1131
2598
|
function resolveOutboundSendCtx(params) {
|
|
1132
2599
|
const { config } = resolveAccount(params.cfg, params.accountId);
|
|
1133
|
-
|
|
2600
|
+
const dialogId = normalizeBitrix24DialogTarget(params.to);
|
|
2601
|
+
if (!config.webhookUrl || !gatewayState || !dialogId)
|
|
1134
2602
|
return null;
|
|
1135
2603
|
return {
|
|
1136
2604
|
webhookUrl: config.webhookUrl,
|
|
1137
2605
|
bot: gatewayState.bot,
|
|
1138
|
-
dialogId
|
|
2606
|
+
dialogId,
|
|
1139
2607
|
};
|
|
1140
2608
|
}
|
|
1141
2609
|
function collectOutboundMediaUrls(input) {
|
|
@@ -1155,16 +2623,22 @@ async function uploadOutboundMedia(params) {
|
|
|
1155
2623
|
let lastMessageId = '';
|
|
1156
2624
|
let message = params.initialMessage;
|
|
1157
2625
|
for (const mediaUrl of params.mediaUrls) {
|
|
2626
|
+
const outboundFileName = resolveSemanticOutboundFileName({
|
|
2627
|
+
requestedFileName: basename(mediaUrl),
|
|
2628
|
+
dialogId: params.sendCtx.dialogId,
|
|
2629
|
+
currentMessageId: params.currentMessageId,
|
|
2630
|
+
accountId: params.accountId,
|
|
2631
|
+
});
|
|
1158
2632
|
const result = await params.mediaService.uploadMediaToChat({
|
|
1159
2633
|
localPath: mediaUrl,
|
|
1160
|
-
fileName:
|
|
2634
|
+
fileName: outboundFileName,
|
|
1161
2635
|
webhookUrl: params.sendCtx.webhookUrl,
|
|
1162
2636
|
bot: params.sendCtx.bot,
|
|
1163
2637
|
dialogId: params.sendCtx.dialogId,
|
|
1164
2638
|
message: message || undefined,
|
|
1165
2639
|
});
|
|
1166
2640
|
if (!result.ok) {
|
|
1167
|
-
throw new Error(`Failed to upload media: ${
|
|
2641
|
+
throw new Error(`Failed to upload media: ${outboundFileName}`);
|
|
1168
2642
|
}
|
|
1169
2643
|
if (result.messageId) {
|
|
1170
2644
|
lastMessageId = String(result.messageId);
|
|
@@ -1196,13 +2670,10 @@ export const bitrix24Plugin = {
|
|
|
1196
2670
|
inlineButtons: 'all',
|
|
1197
2671
|
},
|
|
1198
2672
|
messaging: {
|
|
1199
|
-
normalizeTarget: (raw) => raw
|
|
2673
|
+
normalizeTarget: (raw) => normalizeBitrix24DialogTarget(raw),
|
|
1200
2674
|
targetResolver: {
|
|
1201
|
-
hint: 'Use a numeric
|
|
1202
|
-
looksLikeId: (raw, _normalized) =>
|
|
1203
|
-
const stripped = raw.trim().replace(CHANNEL_PREFIX_RE, '');
|
|
1204
|
-
return /^\d+$/.test(stripped);
|
|
1205
|
-
},
|
|
2675
|
+
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]".',
|
|
2676
|
+
looksLikeId: (raw, _normalized) => looksLikeBitrix24DialogTarget(raw),
|
|
1206
2677
|
},
|
|
1207
2678
|
},
|
|
1208
2679
|
config: {
|
|
@@ -1272,6 +2743,8 @@ export const bitrix24Plugin = {
|
|
|
1272
2743
|
mediaService: gatewayState.mediaService,
|
|
1273
2744
|
sendCtx,
|
|
1274
2745
|
mediaUrls,
|
|
2746
|
+
currentMessageId: ctx.currentMessageId,
|
|
2747
|
+
accountId: ctx.accountId,
|
|
1275
2748
|
initialMessage: ctx.text,
|
|
1276
2749
|
});
|
|
1277
2750
|
return { messageId };
|
|
@@ -1286,27 +2759,49 @@ export const bitrix24Plugin = {
|
|
|
1286
2759
|
const sendCtx = resolveOutboundSendCtx(ctx);
|
|
1287
2760
|
if (!sendCtx || !gatewayState)
|
|
1288
2761
|
throw new Error('Bitrix24 gateway not started');
|
|
2762
|
+
const text = ctx.text;
|
|
1289
2763
|
const keyboard = ctx.payload?.channelData
|
|
1290
2764
|
? extractKeyboardFromPayload({ channelData: ctx.payload.channelData })
|
|
1291
2765
|
: undefined;
|
|
2766
|
+
const attach = ctx.payload?.channelData
|
|
2767
|
+
? extractAttachFromPayload({ channelData: ctx.payload.channelData })
|
|
2768
|
+
: undefined;
|
|
1292
2769
|
const mediaUrls = collectOutboundMediaUrls({
|
|
1293
2770
|
mediaUrl: ctx.mediaUrl,
|
|
1294
2771
|
payload: ctx.payload,
|
|
1295
2772
|
});
|
|
1296
2773
|
if (mediaUrls.length > 0) {
|
|
2774
|
+
const initialMessage = !keyboard && !attach ? text || undefined : undefined;
|
|
1297
2775
|
const uploadedMessageId = await uploadOutboundMedia({
|
|
1298
2776
|
mediaService: gatewayState.mediaService,
|
|
1299
2777
|
sendCtx,
|
|
1300
2778
|
mediaUrls,
|
|
2779
|
+
currentMessageId: ctx.currentMessageId,
|
|
2780
|
+
accountId: ctx.accountId,
|
|
2781
|
+
initialMessage,
|
|
1301
2782
|
});
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
2783
|
+
if ((text && !initialMessage) || keyboard || attach) {
|
|
2784
|
+
if (attach) {
|
|
2785
|
+
const messageId = await gatewayState.api.sendMessage(sendCtx.webhookUrl, sendCtx.bot, sendCtx.dialogId, buildActionMessageText({ text, keyboard, attach }), {
|
|
2786
|
+
...(keyboard ? { keyboard } : {}),
|
|
2787
|
+
attach,
|
|
2788
|
+
});
|
|
2789
|
+
return { messageId: String(messageId || uploadedMessageId) };
|
|
2790
|
+
}
|
|
2791
|
+
const result = await gatewayState.sendService.sendText(sendCtx, text || '', keyboard ? { keyboard } : undefined);
|
|
1304
2792
|
return { messageId: String(result.messageId ?? uploadedMessageId) };
|
|
1305
2793
|
}
|
|
1306
2794
|
return { messageId: uploadedMessageId };
|
|
1307
2795
|
}
|
|
1308
|
-
if (
|
|
1309
|
-
|
|
2796
|
+
if (text || keyboard || attach) {
|
|
2797
|
+
if (attach) {
|
|
2798
|
+
const messageId = await gatewayState.api.sendMessage(sendCtx.webhookUrl, sendCtx.bot, sendCtx.dialogId, buildActionMessageText({ text, keyboard, attach }), {
|
|
2799
|
+
...(keyboard ? { keyboard } : {}),
|
|
2800
|
+
attach,
|
|
2801
|
+
});
|
|
2802
|
+
return { messageId: String(messageId ?? '') };
|
|
2803
|
+
}
|
|
2804
|
+
const result = await gatewayState.sendService.sendText(sendCtx, text || '', keyboard ? { keyboard } : undefined);
|
|
1310
2805
|
return { messageId: String(result.messageId ?? '') };
|
|
1311
2806
|
}
|
|
1312
2807
|
return { messageId: '' };
|
|
@@ -1314,11 +2809,13 @@ export const bitrix24Plugin = {
|
|
|
1314
2809
|
},
|
|
1315
2810
|
// ─── Actions (agent-driven: reactions, etc.) ────────────────────────────
|
|
1316
2811
|
actions: {
|
|
2812
|
+
describeMessageTool: (params) => describeBitrix24MessageTool(params),
|
|
2813
|
+
extractToolSend: (params) => extractBitrix24ToolSend(params.args),
|
|
1317
2814
|
listActions: (_params) => {
|
|
1318
|
-
return [
|
|
2815
|
+
return [...BITRIX24_DISCOVERY_ACTION_NAMES];
|
|
1319
2816
|
},
|
|
1320
2817
|
supportsAction: (params) => {
|
|
1321
|
-
return
|
|
2818
|
+
return BITRIX24_ACTION_NAMES.includes(params.action);
|
|
1322
2819
|
},
|
|
1323
2820
|
handleAction: async (ctx) => {
|
|
1324
2821
|
// Helper: wrap payload as gateway-compatible tool result
|
|
@@ -1326,36 +2823,276 @@ export const bitrix24Plugin = {
|
|
|
1326
2823
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1327
2824
|
details: payload,
|
|
1328
2825
|
});
|
|
2826
|
+
const markSuccessfulAction = (currentMessageId) => {
|
|
2827
|
+
markRecentSuccessfulAction(currentMessageId, ctx.accountId);
|
|
2828
|
+
};
|
|
1329
2829
|
// ─── Send with buttons ──────────────────────────────────────────────
|
|
1330
2830
|
if (ctx.action === 'send') {
|
|
1331
|
-
// Only intercept send when buttons are present; otherwise let gateway handle normally
|
|
1332
2831
|
const rawButtons = ctx.params.buttons;
|
|
1333
|
-
|
|
1334
|
-
return null;
|
|
2832
|
+
const rawAttach = ctx.params.attach ?? ctx.params.attachments;
|
|
1335
2833
|
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
1336
2834
|
if (!config.webhookUrl || !gatewayState)
|
|
1337
2835
|
return null;
|
|
1338
2836
|
const bot = gatewayState.bot;
|
|
1339
|
-
const to =
|
|
2837
|
+
const to = readActionTargetParam(ctx.params, ctx.to);
|
|
1340
2838
|
if (!to) {
|
|
1341
2839
|
defaultLogger.warn('handleAction send: no "to" in params or ctx, falling back to gateway');
|
|
1342
2840
|
return null;
|
|
1343
2841
|
}
|
|
1344
2842
|
const sendCtx = { webhookUrl: config.webhookUrl, bot, dialogId: to };
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2843
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ dialogId: to });
|
|
2844
|
+
const messageText = readActionTextParam(ctx.params);
|
|
2845
|
+
const message = typeof messageText === 'string' ? messageText.trim() : '';
|
|
2846
|
+
const keyboard = parseActionKeyboard(rawButtons);
|
|
2847
|
+
const attach = parseActionAttach(rawAttach);
|
|
2848
|
+
const mediaUrls = collectActionMediaUrls(ctx.params);
|
|
2849
|
+
const toolContext = ctx.toolContext;
|
|
2850
|
+
const currentToolMessageId = toolContext?.currentMessageId;
|
|
2851
|
+
const currentInboundContextKey = buildAccountMessageScopeKey(ctx.accountId, currentToolMessageId);
|
|
2852
|
+
const currentInboundContext = currentInboundContextKey
|
|
2853
|
+
? inboundMessageContextById.get(currentInboundContextKey)
|
|
2854
|
+
: undefined;
|
|
2855
|
+
const forwardAckDialogId = resolvePendingNativeForwardAckDialogId(currentToolMessageId, to, ctx.accountId);
|
|
2856
|
+
const explicitForwardMessages = readExplicitForwardMessageIds(ctx.params);
|
|
2857
|
+
const referencedReplyMessageId = parseActionMessageIds(ctx.params.replyToMessageId ?? ctx.params.replyToId)[0];
|
|
2858
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
2859
|
+
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.' });
|
|
2860
|
+
}
|
|
2861
|
+
if (explicitForwardMessages.length > 0 && mediaUrls.length > 0) {
|
|
2862
|
+
return toolResult({
|
|
2863
|
+
ok: false,
|
|
2864
|
+
reason: 'unsupported_payload_combo',
|
|
2865
|
+
hint: 'Bitrix24 native forward cannot be combined with mediaUrl/mediaUrls uploads in one send action. Send the forward separately.',
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
if (!message && !keyboard && !attach && mediaUrls.length === 0 && explicitForwardMessages.length === 0) {
|
|
2869
|
+
return toolResult({
|
|
2870
|
+
ok: false,
|
|
2871
|
+
reason: 'missing_payload',
|
|
2872
|
+
hint: 'Provide message text, buttons, rich attach blocks or mediaUrl/mediaUrls for Bitrix24 send.',
|
|
2873
|
+
});
|
|
1352
2874
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
2875
|
+
const duplicateMediaDelivery = mediaUrls.length > 0
|
|
2876
|
+
? findRecentMediaDelivery({
|
|
2877
|
+
dialogId: to,
|
|
2878
|
+
currentMessageId: currentToolMessageId,
|
|
2879
|
+
mediaUrls,
|
|
2880
|
+
accountId: ctx.accountId,
|
|
2881
|
+
})
|
|
2882
|
+
: null;
|
|
2883
|
+
if (duplicateMediaDelivery) {
|
|
2884
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2885
|
+
return toolResult({
|
|
2886
|
+
channel: 'bitrix24',
|
|
2887
|
+
to,
|
|
2888
|
+
via: 'direct',
|
|
2889
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
2890
|
+
duplicateSuppressed: true,
|
|
2891
|
+
result: { messageId: duplicateMediaDelivery.messageId ?? '' },
|
|
2892
|
+
});
|
|
1355
2893
|
}
|
|
1356
|
-
const keyboard = buttons?.length ? convertButtonsToKeyboard(buttons) : undefined;
|
|
1357
2894
|
try {
|
|
2895
|
+
if (explicitForwardMessages.length > 0) {
|
|
2896
|
+
const normalizedForwardText = normalizeForwardActionText(messageText);
|
|
2897
|
+
if (attach) {
|
|
2898
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: normalizedForwardText, keyboard, attach }), {
|
|
2899
|
+
...(keyboard ? { keyboard } : {}),
|
|
2900
|
+
attach,
|
|
2901
|
+
forwardMessages: explicitForwardMessages,
|
|
2902
|
+
});
|
|
2903
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
2904
|
+
api: gatewayState.api,
|
|
2905
|
+
sendService: gatewayState.sendService,
|
|
2906
|
+
webhookUrl: config.webhookUrl,
|
|
2907
|
+
bot,
|
|
2908
|
+
dialogId: forwardAckDialogId,
|
|
2909
|
+
currentMessageId: currentToolMessageId,
|
|
2910
|
+
});
|
|
2911
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2912
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2913
|
+
return toolResult({
|
|
2914
|
+
channel: 'bitrix24',
|
|
2915
|
+
to,
|
|
2916
|
+
via: 'direct',
|
|
2917
|
+
mediaUrl: null,
|
|
2918
|
+
forwarded: true,
|
|
2919
|
+
forwardMessages: explicitForwardMessages,
|
|
2920
|
+
result: { messageId: String(messageId ?? '') },
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
const result = await gatewayState.sendService.sendText(sendCtx, normalizedForwardText || (keyboard ? ' ' : ''), {
|
|
2924
|
+
...(keyboard ? { keyboard } : {}),
|
|
2925
|
+
forwardMessages: explicitForwardMessages,
|
|
2926
|
+
});
|
|
2927
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
2928
|
+
api: gatewayState.api,
|
|
2929
|
+
sendService: gatewayState.sendService,
|
|
2930
|
+
webhookUrl: config.webhookUrl,
|
|
2931
|
+
bot,
|
|
2932
|
+
dialogId: forwardAckDialogId,
|
|
2933
|
+
currentMessageId: currentToolMessageId,
|
|
2934
|
+
});
|
|
2935
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2936
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2937
|
+
return toolResult({
|
|
2938
|
+
channel: 'bitrix24',
|
|
2939
|
+
to,
|
|
2940
|
+
via: 'direct',
|
|
2941
|
+
mediaUrl: null,
|
|
2942
|
+
forwarded: true,
|
|
2943
|
+
forwardMessages: explicitForwardMessages,
|
|
2944
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
if (canUseNativeReply && referencedReplyMessageId && !attach && mediaUrls.length === 0) {
|
|
2948
|
+
let referencedMessageText = '';
|
|
2949
|
+
try {
|
|
2950
|
+
const referencedMessage = await gatewayState.api.getMessage(config.webhookUrl, bot, referencedReplyMessageId);
|
|
2951
|
+
referencedMessageText = resolveFetchedMessageBody(referencedMessage.message?.text ?? '');
|
|
2952
|
+
}
|
|
2953
|
+
catch (err) {
|
|
2954
|
+
defaultLogger.debug('Failed to hydrate referenced Bitrix24 message for send action', err);
|
|
2955
|
+
}
|
|
2956
|
+
const normalizedReferencedMessage = normalizeComparableMessageText(referencedMessageText);
|
|
2957
|
+
const normalizedMessage = normalizeComparableMessageText(message);
|
|
2958
|
+
if (normalizedReferencedMessage && normalizedReferencedMessage === normalizedMessage) {
|
|
2959
|
+
const result = await gatewayState.sendService.sendText(sendCtx, '', { forwardMessages: [referencedReplyMessageId] });
|
|
2960
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
2961
|
+
api: gatewayState.api,
|
|
2962
|
+
sendService: gatewayState.sendService,
|
|
2963
|
+
webhookUrl: config.webhookUrl,
|
|
2964
|
+
bot,
|
|
2965
|
+
dialogId: forwardAckDialogId,
|
|
2966
|
+
currentMessageId: currentToolMessageId,
|
|
2967
|
+
});
|
|
2968
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2969
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2970
|
+
return toolResult({
|
|
2971
|
+
channel: 'bitrix24',
|
|
2972
|
+
to,
|
|
2973
|
+
via: 'direct',
|
|
2974
|
+
mediaUrl: null,
|
|
2975
|
+
forwarded: true,
|
|
2976
|
+
forwardMessages: [referencedReplyMessageId],
|
|
2977
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
const sendOptions = {
|
|
2981
|
+
...(keyboard ? { keyboard } : {}),
|
|
2982
|
+
...(canUseNativeReply ? { replyToMessageId: referencedReplyMessageId } : {}),
|
|
2983
|
+
};
|
|
2984
|
+
const result = await gatewayState.sendService.sendText(sendCtx, message || ' ', Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
2985
|
+
markSuccessfulAction(currentToolMessageId);
|
|
2986
|
+
return toolResult({
|
|
2987
|
+
channel: 'bitrix24',
|
|
2988
|
+
to,
|
|
2989
|
+
via: 'direct',
|
|
2990
|
+
mediaUrl: null,
|
|
2991
|
+
...(canUseNativeReply ? {
|
|
2992
|
+
replied: true,
|
|
2993
|
+
replyToMessageId: referencedReplyMessageId,
|
|
2994
|
+
} : {}),
|
|
2995
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(currentToolMessageId, ctx.accountId);
|
|
2999
|
+
if (!referencedReplyMessageId
|
|
3000
|
+
&& !attach
|
|
3001
|
+
&& mediaUrls.length === 0
|
|
3002
|
+
&& !keyboard
|
|
3003
|
+
&& previousInboundMessage
|
|
3004
|
+
&& currentInboundContext
|
|
3005
|
+
&& normalizeComparableMessageText(message)
|
|
3006
|
+
&& normalizeComparableMessageText(message) === normalizeComparableMessageText(previousInboundMessage.body)
|
|
3007
|
+
&& normalizeComparableMessageText(currentInboundContext.body) !== normalizeComparableMessageText(message)) {
|
|
3008
|
+
const result = await gatewayState.sendService.sendText(sendCtx, '', { forwardMessages: [Number(previousInboundMessage.messageId)] });
|
|
3009
|
+
await maybeAddNativeForwardSuccessReaction({
|
|
3010
|
+
api: gatewayState.api,
|
|
3011
|
+
sendService: gatewayState.sendService,
|
|
3012
|
+
webhookUrl: config.webhookUrl,
|
|
3013
|
+
bot,
|
|
3014
|
+
dialogId: forwardAckDialogId,
|
|
3015
|
+
currentMessageId: currentToolMessageId,
|
|
3016
|
+
});
|
|
3017
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
3018
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3019
|
+
return toolResult({
|
|
3020
|
+
channel: 'bitrix24',
|
|
3021
|
+
to,
|
|
3022
|
+
via: 'direct',
|
|
3023
|
+
mediaUrl: null,
|
|
3024
|
+
forwarded: true,
|
|
3025
|
+
forwardMessages: [Number(previousInboundMessage.messageId)],
|
|
3026
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
if (mediaUrls.length > 0) {
|
|
3030
|
+
const initialMessage = !keyboard && !attach ? message || undefined : undefined;
|
|
3031
|
+
const uploadedMessageId = await uploadOutboundMedia({
|
|
3032
|
+
mediaService: gatewayState.mediaService,
|
|
3033
|
+
sendCtx,
|
|
3034
|
+
mediaUrls,
|
|
3035
|
+
currentMessageId: currentToolMessageId,
|
|
3036
|
+
accountId: ctx.accountId,
|
|
3037
|
+
initialMessage,
|
|
3038
|
+
});
|
|
3039
|
+
markRecentMediaDelivery({
|
|
3040
|
+
dialogId: to,
|
|
3041
|
+
currentMessageId: currentToolMessageId,
|
|
3042
|
+
mediaUrls,
|
|
3043
|
+
accountId: ctx.accountId,
|
|
3044
|
+
messageId: uploadedMessageId,
|
|
3045
|
+
});
|
|
3046
|
+
if ((message && !initialMessage) || keyboard || attach) {
|
|
3047
|
+
if (attach) {
|
|
3048
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: message, keyboard, attach }), {
|
|
3049
|
+
...(keyboard ? { keyboard } : {}),
|
|
3050
|
+
attach,
|
|
3051
|
+
});
|
|
3052
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3053
|
+
return toolResult({
|
|
3054
|
+
channel: 'bitrix24',
|
|
3055
|
+
to,
|
|
3056
|
+
via: 'direct',
|
|
3057
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
3058
|
+
result: { messageId: String(messageId ?? uploadedMessageId) },
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
const result = await gatewayState.sendService.sendText(sendCtx, message || '', keyboard ? { keyboard } : undefined);
|
|
3062
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3063
|
+
return toolResult({
|
|
3064
|
+
channel: 'bitrix24',
|
|
3065
|
+
to,
|
|
3066
|
+
via: 'direct',
|
|
3067
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
3068
|
+
result: { messageId: String(result.messageId ?? uploadedMessageId) },
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3072
|
+
return toolResult({
|
|
3073
|
+
channel: 'bitrix24',
|
|
3074
|
+
to,
|
|
3075
|
+
via: 'direct',
|
|
3076
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
3077
|
+
result: { messageId: uploadedMessageId },
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
if (attach) {
|
|
3081
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: message, keyboard, attach }), {
|
|
3082
|
+
...(keyboard ? { keyboard } : {}),
|
|
3083
|
+
attach,
|
|
3084
|
+
});
|
|
3085
|
+
markSuccessfulAction(currentToolMessageId);
|
|
3086
|
+
return toolResult({
|
|
3087
|
+
channel: 'bitrix24',
|
|
3088
|
+
to,
|
|
3089
|
+
via: 'direct',
|
|
3090
|
+
mediaUrl: null,
|
|
3091
|
+
result: { messageId: String(messageId ?? '') },
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
1358
3094
|
const result = await gatewayState.sendService.sendText(sendCtx, message || ' ', keyboard ? { keyboard } : undefined);
|
|
3095
|
+
markSuccessfulAction(currentToolMessageId);
|
|
1359
3096
|
return toolResult({
|
|
1360
3097
|
channel: 'bitrix24',
|
|
1361
3098
|
to,
|
|
@@ -1366,9 +3103,174 @@ export const bitrix24Plugin = {
|
|
|
1366
3103
|
}
|
|
1367
3104
|
catch (err) {
|
|
1368
3105
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3106
|
+
if (errMsg.startsWith('Failed to upload media: ')) {
|
|
3107
|
+
return toolResult({
|
|
3108
|
+
ok: false,
|
|
3109
|
+
reason: 'media_upload_failed',
|
|
3110
|
+
error: errMsg,
|
|
3111
|
+
hint: 'The local file could not be uploaded. First create/write the file in the OpenClaw workspace or managed media directory, then send it once with media/filePath/path. Do not retry the same send blindly, and do not send an extra confirmation after a successful file upload.',
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
1369
3114
|
return toolResult({ ok: false, error: errMsg });
|
|
1370
3115
|
}
|
|
1371
3116
|
}
|
|
3117
|
+
if (ctx.action === 'reply') {
|
|
3118
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
3119
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
3120
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
3121
|
+
}
|
|
3122
|
+
const params = ctx.params;
|
|
3123
|
+
const bot = gatewayState.bot;
|
|
3124
|
+
const to = readActionTargetParam(params, ctx.to);
|
|
3125
|
+
if (!to) {
|
|
3126
|
+
return toolResult({ ok: false, reason: 'missing_target', hint: 'Bitrix24 reply requires a target dialog id in "to" or "target". Do not retry.' });
|
|
3127
|
+
}
|
|
3128
|
+
const toolContext = ctx.toolContext;
|
|
3129
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ dialogId: to });
|
|
3130
|
+
const replyToMessageId = parseActionMessageIds(params.replyToMessageId
|
|
3131
|
+
?? params.replyToId
|
|
3132
|
+
?? params.messageId
|
|
3133
|
+
?? params.message_id
|
|
3134
|
+
?? toolContext?.currentMessageId)[0];
|
|
3135
|
+
if (!replyToMessageId) {
|
|
3136
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Bitrix24 reply requires a valid target messageId. Do not retry.' });
|
|
3137
|
+
}
|
|
3138
|
+
const text = readActionTextParam(params);
|
|
3139
|
+
const normalizedText = typeof text === 'string' ? text.trim() : '';
|
|
3140
|
+
const keyboard = parseActionKeyboard(params.buttons);
|
|
3141
|
+
const rawAttach = params.attach ?? params.attachments;
|
|
3142
|
+
const attach = parseActionAttach(rawAttach);
|
|
3143
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
3144
|
+
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.' });
|
|
3145
|
+
}
|
|
3146
|
+
if (!normalizedText && !keyboard && !attach) {
|
|
3147
|
+
return toolResult({ ok: false, reason: 'missing_payload', hint: 'Provide reply text, buttons or rich attach blocks for Bitrix24 reply.' });
|
|
3148
|
+
}
|
|
3149
|
+
try {
|
|
3150
|
+
if (attach) {
|
|
3151
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: normalizedText, keyboard, attach }), {
|
|
3152
|
+
...(keyboard ? { keyboard } : {}),
|
|
3153
|
+
attach,
|
|
3154
|
+
...(canUseNativeReply ? { replyToMessageId } : {}),
|
|
3155
|
+
});
|
|
3156
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3157
|
+
return toolResult({
|
|
3158
|
+
ok: true,
|
|
3159
|
+
to,
|
|
3160
|
+
...(canUseNativeReply ? {
|
|
3161
|
+
replied: true,
|
|
3162
|
+
replyToMessageId,
|
|
3163
|
+
} : {}),
|
|
3164
|
+
messageId: String(messageId ?? ''),
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
const sendOptions = {
|
|
3168
|
+
...(keyboard ? { keyboard } : {}),
|
|
3169
|
+
...(canUseNativeReply ? { replyToMessageId } : {}),
|
|
3170
|
+
};
|
|
3171
|
+
const result = await gatewayState.sendService.sendText({ webhookUrl: config.webhookUrl, bot, dialogId: to }, normalizedText || ' ', Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
3172
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3173
|
+
return toolResult({
|
|
3174
|
+
ok: true,
|
|
3175
|
+
to,
|
|
3176
|
+
...(canUseNativeReply ? {
|
|
3177
|
+
replied: true,
|
|
3178
|
+
replyToMessageId,
|
|
3179
|
+
} : {}),
|
|
3180
|
+
messageId: String(result.messageId ?? ''),
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
catch (err) {
|
|
3184
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3185
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to send Bitrix24 reply: ${errMsg}. Do not retry.` });
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
// Note: "forward" action is not supported by OpenClaw runtime (not in
|
|
3189
|
+
// MESSAGE_ACTION_TARGET_MODE), so the runtime blocks target before it
|
|
3190
|
+
// reaches handleAction. All forwarding goes through action "send" with
|
|
3191
|
+
// forwardMessageIds parameter instead.
|
|
3192
|
+
if (ctx.action === 'edit') {
|
|
3193
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
3194
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
3195
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
3196
|
+
}
|
|
3197
|
+
const bot = gatewayState.bot;
|
|
3198
|
+
const api = gatewayState.api;
|
|
3199
|
+
const params = ctx.params;
|
|
3200
|
+
const toolContext = ctx.toolContext;
|
|
3201
|
+
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
3202
|
+
const messageId = Number(rawMessageId);
|
|
3203
|
+
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
3204
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 edits. Do not retry.' });
|
|
3205
|
+
}
|
|
3206
|
+
let keyboard;
|
|
3207
|
+
if (params.buttons === 'N') {
|
|
3208
|
+
keyboard = 'N';
|
|
3209
|
+
}
|
|
3210
|
+
else if (params.buttons != null) {
|
|
3211
|
+
try {
|
|
3212
|
+
const parsed = typeof params.buttons === 'string'
|
|
3213
|
+
? JSON.parse(params.buttons)
|
|
3214
|
+
: params.buttons;
|
|
3215
|
+
if (Array.isArray(parsed)) {
|
|
3216
|
+
keyboard = convertButtonsToKeyboard(parsed);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
catch {
|
|
3220
|
+
// Ignore invalid buttons payload and update text only.
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
const message = readActionTextParam(params);
|
|
3224
|
+
const rawAttach = params.attach ?? params.attachments;
|
|
3225
|
+
const attach = parseActionAttach(rawAttach);
|
|
3226
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
3227
|
+
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.' });
|
|
3228
|
+
}
|
|
3229
|
+
if (message == null && !keyboard && !attach) {
|
|
3230
|
+
return toolResult({ ok: false, reason: 'missing_payload', hint: 'Provide message text, buttons or rich attach blocks for Bitrix24 edits.' });
|
|
3231
|
+
}
|
|
3232
|
+
try {
|
|
3233
|
+
await api.updateMessage(config.webhookUrl, bot, messageId, message, {
|
|
3234
|
+
...(keyboard ? { keyboard } : {}),
|
|
3235
|
+
...(attach ? { attach } : {}),
|
|
3236
|
+
});
|
|
3237
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3238
|
+
return toolResult({ ok: true, edited: true, messageId });
|
|
3239
|
+
}
|
|
3240
|
+
catch (err) {
|
|
3241
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3242
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to edit message: ${errMsg}. Do not retry.` });
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
if (ctx.action === 'delete') {
|
|
3246
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
3247
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
3248
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
3249
|
+
}
|
|
3250
|
+
const bot = gatewayState.bot;
|
|
3251
|
+
const api = gatewayState.api;
|
|
3252
|
+
const params = ctx.params;
|
|
3253
|
+
const toolContext = ctx.toolContext;
|
|
3254
|
+
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
3255
|
+
const messageId = Number(rawMessageId);
|
|
3256
|
+
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
3257
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 deletes. Do not retry.' });
|
|
3258
|
+
}
|
|
3259
|
+
const complete = params.complete == null
|
|
3260
|
+
? undefined
|
|
3261
|
+
: (params.complete === true
|
|
3262
|
+
|| params.complete === 'true'
|
|
3263
|
+
|| params.complete === 'Y');
|
|
3264
|
+
try {
|
|
3265
|
+
await api.deleteMessage(config.webhookUrl, bot, messageId, complete);
|
|
3266
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
3267
|
+
return toolResult({ ok: true, deleted: true, messageId });
|
|
3268
|
+
}
|
|
3269
|
+
catch (err) {
|
|
3270
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3271
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to delete message: ${errMsg}. Do not retry.` });
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
1372
3274
|
// ─── React ────────────────────────────────────────────────────────────
|
|
1373
3275
|
if (ctx.action !== 'react')
|
|
1374
3276
|
return null;
|
|
@@ -1383,6 +3285,10 @@ export const bitrix24Plugin = {
|
|
|
1383
3285
|
const toolContext = ctx.toolContext;
|
|
1384
3286
|
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
1385
3287
|
const messageId = Number(rawMessageId);
|
|
3288
|
+
const reactionAckSourceMessageId = typeof rawMessageId === 'string' || typeof rawMessageId === 'number'
|
|
3289
|
+
? rawMessageId
|
|
3290
|
+
: toolContext?.currentMessageId;
|
|
3291
|
+
const reactionAckDialogId = resolveRecentInboundDialogId(reactionAckSourceMessageId, ctx.accountId);
|
|
1386
3292
|
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
1387
3293
|
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 reactions. Do not retry.' });
|
|
1388
3294
|
}
|
|
@@ -1396,6 +3302,7 @@ export const bitrix24Plugin = {
|
|
|
1396
3302
|
}
|
|
1397
3303
|
try {
|
|
1398
3304
|
await api.deleteReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
3305
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
1399
3306
|
return toolResult({ ok: true, removed: true });
|
|
1400
3307
|
}
|
|
1401
3308
|
catch (err) {
|
|
@@ -1418,12 +3325,28 @@ export const bitrix24Plugin = {
|
|
|
1418
3325
|
}
|
|
1419
3326
|
try {
|
|
1420
3327
|
await api.addReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
3328
|
+
await maybeSendReactionTypingReset({
|
|
3329
|
+
sendService: gatewayState.sendService,
|
|
3330
|
+
webhookUrl: config.webhookUrl,
|
|
3331
|
+
bot,
|
|
3332
|
+
dialogId: reactionAckDialogId,
|
|
3333
|
+
});
|
|
3334
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
3335
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
1421
3336
|
return toolResult({ ok: true, added: emoji });
|
|
1422
3337
|
}
|
|
1423
3338
|
catch (err) {
|
|
1424
3339
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1425
3340
|
const isAlreadySet = errMsg.includes('REACTION_ALREADY_SET');
|
|
1426
3341
|
if (isAlreadySet) {
|
|
3342
|
+
await maybeSendReactionTypingReset({
|
|
3343
|
+
sendService: gatewayState.sendService,
|
|
3344
|
+
webhookUrl: config.webhookUrl,
|
|
3345
|
+
bot,
|
|
3346
|
+
dialogId: reactionAckDialogId,
|
|
3347
|
+
});
|
|
3348
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
3349
|
+
markSuccessfulAction(toolContext?.currentMessageId);
|
|
1427
3350
|
return toolResult({ ok: true, added: emoji, warning: 'Reaction already set.' });
|
|
1428
3351
|
}
|
|
1429
3352
|
return toolResult({ ok: false, reason: 'error', emoji, hint: `Reaction failed: ${errMsg}. Do not retry.` });
|
|
@@ -1561,6 +3484,30 @@ export const bitrix24Plugin = {
|
|
|
1561
3484
|
}
|
|
1562
3485
|
const sendService = new SendService(api, logger);
|
|
1563
3486
|
const mediaService = new MediaService(api, logger);
|
|
3487
|
+
const originalSendText = sendService.sendText.bind(sendService);
|
|
3488
|
+
sendService.sendText = async (...args) => {
|
|
3489
|
+
const result = await originalSendText(...args);
|
|
3490
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
3491
|
+
return result;
|
|
3492
|
+
};
|
|
3493
|
+
const originalAnswerCommandText = sendService.answerCommandText.bind(sendService);
|
|
3494
|
+
sendService.answerCommandText = async (...args) => {
|
|
3495
|
+
const result = await originalAnswerCommandText(...args);
|
|
3496
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
3497
|
+
return result;
|
|
3498
|
+
};
|
|
3499
|
+
const originalApiSendMessage = api.sendMessage.bind(api);
|
|
3500
|
+
api.sendMessage = async (...args) => {
|
|
3501
|
+
const messageId = await originalApiSendMessage(...args);
|
|
3502
|
+
markRecentOutboundMessage(messageId, ctx.accountId);
|
|
3503
|
+
return messageId;
|
|
3504
|
+
};
|
|
3505
|
+
const originalUploadMediaToChat = mediaService.uploadMediaToChat.bind(mediaService);
|
|
3506
|
+
mediaService.uploadMediaToChat = async (...args) => {
|
|
3507
|
+
const result = await originalUploadMediaToChat(...args);
|
|
3508
|
+
markRecentOutboundMessage(result?.messageId, ctx.accountId);
|
|
3509
|
+
return result;
|
|
3510
|
+
};
|
|
1564
3511
|
const hydrateReplyEntry = async (replyToMessageId) => {
|
|
1565
3512
|
const bitrixMessageId = toMessageId(replyToMessageId);
|
|
1566
3513
|
if (!bitrixMessageId) {
|
|
@@ -1723,9 +3670,27 @@ export const bitrix24Plugin = {
|
|
|
1723
3670
|
query: bodyWithReply,
|
|
1724
3671
|
historyCache,
|
|
1725
3672
|
});
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
3673
|
+
const inlineButtonsAgentHint = buildBitrix24InlineButtonsAgentHint();
|
|
3674
|
+
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
3675
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ isDm: msgCtx.isDm });
|
|
3676
|
+
const nativeReplyAgentHint = canUseNativeReply
|
|
3677
|
+
? buildBitrix24NativeReplyAgentHint(msgCtx.messageId)
|
|
3678
|
+
: undefined;
|
|
3679
|
+
const nativeForwardAgentHint = buildBitrix24NativeForwardAgentHint({
|
|
3680
|
+
accountId: ctx.accountId,
|
|
3681
|
+
currentBody: body,
|
|
3682
|
+
currentMessageId: msgCtx.messageId,
|
|
3683
|
+
replyToMessageId: msgCtx.replyToMessageId,
|
|
3684
|
+
historyEntries: previousEntries,
|
|
3685
|
+
});
|
|
3686
|
+
const bodyForAgent = [
|
|
3687
|
+
inlineButtonsAgentHint,
|
|
3688
|
+
fileDeliveryAgentHint,
|
|
3689
|
+
nativeReplyAgentHint,
|
|
3690
|
+
nativeForwardAgentHint,
|
|
3691
|
+
crossChatContext,
|
|
3692
|
+
bodyWithReply,
|
|
3693
|
+
].filter(Boolean).join('\n\n');
|
|
1729
3694
|
const combinedBody = msgCtx.isGroup
|
|
1730
3695
|
? buildHistoryContext({
|
|
1731
3696
|
entries: previousEntries,
|
|
@@ -1733,6 +3698,7 @@ export const bitrix24Plugin = {
|
|
|
1733
3698
|
})
|
|
1734
3699
|
: bodyForAgent;
|
|
1735
3700
|
recordHistory(body);
|
|
3701
|
+
rememberRecentInboundMessage(conversation.dialogId, msgCtx.messageId, body, msgCtx.media.map((mediaItem) => mediaItem.name), msgCtx.timestamp ?? Date.now(), ctx.accountId);
|
|
1736
3702
|
// Resolve which agent handles this conversation
|
|
1737
3703
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
1738
3704
|
cfg,
|
|
@@ -1785,15 +3751,56 @@ export const bitrix24Plugin = {
|
|
|
1785
3751
|
deliver: async (payload) => {
|
|
1786
3752
|
await replyStatusHeartbeat.stopAndWait();
|
|
1787
3753
|
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
3754
|
+
const hadRecentMediaDelivery = mediaUrls.length === 0
|
|
3755
|
+
&& hasRecentMediaDelivery(sendCtx.dialogId, msgCtx.messageId, ctx.accountId);
|
|
1788
3756
|
if (mediaUrls.length > 0) {
|
|
1789
|
-
await uploadOutboundMedia({
|
|
3757
|
+
const uploadedMessageId = await uploadOutboundMedia({
|
|
1790
3758
|
mediaService,
|
|
1791
3759
|
sendCtx,
|
|
1792
3760
|
mediaUrls,
|
|
1793
3761
|
});
|
|
3762
|
+
markRecentMediaDelivery({
|
|
3763
|
+
dialogId: sendCtx.dialogId,
|
|
3764
|
+
currentMessageId: msgCtx.messageId,
|
|
3765
|
+
mediaUrls,
|
|
3766
|
+
accountId: ctx.accountId,
|
|
3767
|
+
messageId: uploadedMessageId,
|
|
3768
|
+
});
|
|
1794
3769
|
}
|
|
1795
3770
|
if (payload.text) {
|
|
1796
|
-
|
|
3771
|
+
if (hadRecentMediaDelivery) {
|
|
3772
|
+
replyDelivered = true;
|
|
3773
|
+
logger.debug('Suppressing trailing text after successful Bitrix24 file upload', {
|
|
3774
|
+
senderId: msgCtx.senderId,
|
|
3775
|
+
chatId: msgCtx.chatId,
|
|
3776
|
+
messageId: msgCtx.messageId,
|
|
3777
|
+
});
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
if (consumePendingNativeForwardAck(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
3781
|
+
replyDelivered = true;
|
|
3782
|
+
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 forward', {
|
|
3783
|
+
senderId: msgCtx.senderId,
|
|
3784
|
+
chatId: msgCtx.chatId,
|
|
3785
|
+
messageId: msgCtx.messageId,
|
|
3786
|
+
});
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
const replyDirective = extractReplyDirective({
|
|
3790
|
+
text: payload.text,
|
|
3791
|
+
currentMessageId: msgCtx.messageId,
|
|
3792
|
+
explicitReplyToId: payload.replyToId,
|
|
3793
|
+
});
|
|
3794
|
+
let text = replyDirective.cleanText;
|
|
3795
|
+
if (consumePendingNativeReactionAck(msgCtx.messageId, text, ctx.accountId)) {
|
|
3796
|
+
replyDelivered = true;
|
|
3797
|
+
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 reaction', {
|
|
3798
|
+
senderId: msgCtx.senderId,
|
|
3799
|
+
chatId: msgCtx.chatId,
|
|
3800
|
+
messageId: msgCtx.messageId,
|
|
3801
|
+
});
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
1797
3804
|
let keyboard = extractKeyboardFromPayload(payload);
|
|
1798
3805
|
// Fallback: agent may embed button JSON in text as [[{...}]]
|
|
1799
3806
|
if (!keyboard) {
|
|
@@ -1803,8 +3810,52 @@ export const bitrix24Plugin = {
|
|
|
1803
3810
|
keyboard = extracted.keyboard;
|
|
1804
3811
|
}
|
|
1805
3812
|
}
|
|
3813
|
+
const nativeForwardMessageId = findNativeForwardTarget({
|
|
3814
|
+
accountId: ctx.accountId,
|
|
3815
|
+
requestText: body,
|
|
3816
|
+
deliveredText: text,
|
|
3817
|
+
historyEntries: previousEntries,
|
|
3818
|
+
currentMessageId: msgCtx.messageId,
|
|
3819
|
+
}) ?? findImplicitForwardTargetFromRecentInbound({
|
|
3820
|
+
accountId: ctx.accountId,
|
|
3821
|
+
requestText: body,
|
|
3822
|
+
deliveredText: text,
|
|
3823
|
+
currentMessageId: msgCtx.messageId,
|
|
3824
|
+
historyEntries: previousEntries,
|
|
3825
|
+
});
|
|
3826
|
+
const nativeForwardTargets = nativeForwardMessageId
|
|
3827
|
+
? extractBitrix24DialogTargetsFromText(body)
|
|
3828
|
+
: [];
|
|
3829
|
+
const nativeReplyToMessageId = nativeForwardMessageId
|
|
3830
|
+
? undefined
|
|
3831
|
+
: (canUseNativeReply ? replyDirective.replyToMessageId : undefined);
|
|
3832
|
+
const sendOptions = {
|
|
3833
|
+
...(keyboard ? { keyboard } : {}),
|
|
3834
|
+
...(nativeReplyToMessageId ? { replyToMessageId: nativeReplyToMessageId } : {}),
|
|
3835
|
+
...(nativeForwardMessageId ? { forwardMessages: [nativeForwardMessageId] } : {}),
|
|
3836
|
+
};
|
|
1806
3837
|
replyDelivered = true;
|
|
1807
|
-
|
|
3838
|
+
if (nativeForwardMessageId && nativeForwardTargets.length > 0) {
|
|
3839
|
+
const forwardResults = await Promise.allSettled(nativeForwardTargets.map((dialogId) => sendService.sendText({ ...sendCtx, dialogId }, '', { forwardMessages: [nativeForwardMessageId] })));
|
|
3840
|
+
const firstFailure = forwardResults.find((result) => result.status === 'rejected');
|
|
3841
|
+
const hasSuccess = forwardResults.some((result) => result.status === 'fulfilled');
|
|
3842
|
+
if (firstFailure && !hasSuccess) {
|
|
3843
|
+
throw firstFailure.reason;
|
|
3844
|
+
}
|
|
3845
|
+
if (firstFailure) {
|
|
3846
|
+
logger.warn('Failed to deliver Bitrix24 native forward to one or more explicit targets', {
|
|
3847
|
+
senderId: msgCtx.senderId,
|
|
3848
|
+
chatId: msgCtx.chatId,
|
|
3849
|
+
messageId: msgCtx.messageId,
|
|
3850
|
+
targets: nativeForwardTargets,
|
|
3851
|
+
error: firstFailure.reason instanceof Error
|
|
3852
|
+
? firstFailure.reason.message
|
|
3853
|
+
: String(firstFailure.reason),
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
await sendService.sendText(sendCtx, nativeForwardMessageId ? '' : text, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
1808
3859
|
}
|
|
1809
3860
|
},
|
|
1810
3861
|
onReplyStart: async () => {
|
|
@@ -1815,18 +3866,55 @@ export const bitrix24Plugin = {
|
|
|
1815
3866
|
},
|
|
1816
3867
|
},
|
|
1817
3868
|
});
|
|
3869
|
+
if (!replyDelivered && hasRecentMediaDelivery(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
3870
|
+
replyDelivered = true;
|
|
3871
|
+
logger.debug('Suppressing fallback after successful Bitrix24 file upload in the same turn', {
|
|
3872
|
+
senderId: msgCtx.senderId,
|
|
3873
|
+
chatId: msgCtx.chatId,
|
|
3874
|
+
messageId: msgCtx.messageId,
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
if (!replyDelivered && hasRecentSuccessfulAction(msgCtx.messageId, ctx.accountId)) {
|
|
3878
|
+
replyDelivered = true;
|
|
3879
|
+
logger.debug('Suppressing fallback after successful Bitrix24 action in the same turn', {
|
|
3880
|
+
senderId: msgCtx.senderId,
|
|
3881
|
+
chatId: msgCtx.chatId,
|
|
3882
|
+
messageId: msgCtx.messageId,
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
const shouldWaitForEmptyReplyFallback = !replyDelivered
|
|
3886
|
+
&& isEmptyDispatchResult(dispatchResult);
|
|
1818
3887
|
if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
1819
3888
|
logger.debug('Reply completed without queued final block, waiting before fallback', {
|
|
1820
3889
|
senderId: msgCtx.senderId,
|
|
1821
3890
|
chatId: msgCtx.chatId,
|
|
1822
3891
|
counts: dispatchResult.counts,
|
|
3892
|
+
fallbackDelayMs: shouldWaitForEmptyReplyFallback
|
|
3893
|
+
? EMPTY_REPLY_FALLBACK_GRACE_MS
|
|
3894
|
+
: REPLY_FALLBACK_GRACE_MS,
|
|
3895
|
+
});
|
|
3896
|
+
if (shouldWaitForEmptyReplyFallback) {
|
|
3897
|
+
await waitEmptyReplyFallbackGraceWindow();
|
|
3898
|
+
}
|
|
3899
|
+
else {
|
|
3900
|
+
await waitReplyFallbackGraceWindow();
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
if (!replyDelivered && isEmptyDispatchResult(dispatchResult)) {
|
|
3904
|
+
replyDelivered = true;
|
|
3905
|
+
logger.warn('Reply completed without any user-visible payload or tool activity; sending fallback notice', {
|
|
3906
|
+
senderId: msgCtx.senderId,
|
|
3907
|
+
chatId: msgCtx.chatId,
|
|
3908
|
+
messageId: msgCtx.messageId,
|
|
3909
|
+
counts: dispatchResult.counts,
|
|
1823
3910
|
});
|
|
1824
|
-
await
|
|
3911
|
+
await sendService.sendText(sendCtx, emptyReplyFallback(msgCtx.language));
|
|
1825
3912
|
}
|
|
1826
|
-
if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3913
|
+
else if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
1827
3914
|
logger.warn('Reply completed without a final user-visible message; fallback notice suppressed', {
|
|
1828
3915
|
senderId: msgCtx.senderId,
|
|
1829
3916
|
chatId: msgCtx.chatId,
|
|
3917
|
+
messageId: msgCtx.messageId,
|
|
1830
3918
|
counts: dispatchResult.counts,
|
|
1831
3919
|
});
|
|
1832
3920
|
}
|
|
@@ -1834,6 +3922,7 @@ export const bitrix24Plugin = {
|
|
|
1834
3922
|
logger.debug('Late reply arrived during fallback grace window, skipping fallback', {
|
|
1835
3923
|
senderId: msgCtx.senderId,
|
|
1836
3924
|
chatId: msgCtx.chatId,
|
|
3925
|
+
messageId: msgCtx.messageId,
|
|
1837
3926
|
});
|
|
1838
3927
|
}
|
|
1839
3928
|
}
|
|
@@ -1852,7 +3941,7 @@ export const bitrix24Plugin = {
|
|
|
1852
3941
|
}
|
|
1853
3942
|
}
|
|
1854
3943
|
finally {
|
|
1855
|
-
|
|
3944
|
+
mediaService.scheduleDownloadedMediaCleanup(downloadedMedia.map((mediaItem) => mediaItem.path));
|
|
1856
3945
|
}
|
|
1857
3946
|
};
|
|
1858
3947
|
const directTextCoalescer = new BufferedDirectMessageCoalescer({
|
|
@@ -2005,6 +4094,15 @@ export const bitrix24Plugin = {
|
|
|
2005
4094
|
messageId: msgCtx.messageId,
|
|
2006
4095
|
textLen: msgCtx.text.length,
|
|
2007
4096
|
});
|
|
4097
|
+
if (hasRecentOutboundMessage(msgCtx.messageId, ctx.accountId)) {
|
|
4098
|
+
logger.debug('Skipping recent outbound Bitrix24 message echo', {
|
|
4099
|
+
senderId: msgCtx.senderId,
|
|
4100
|
+
chatId: msgCtx.chatId,
|
|
4101
|
+
messageId: msgCtx.messageId,
|
|
4102
|
+
eventScope: msgCtx.eventScope,
|
|
4103
|
+
});
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
2008
4106
|
const pendingForwardContext = msgCtx.isForwarded
|
|
2009
4107
|
? directTextCoalescer.take(ctx.accountId, msgCtx.chatId)
|
|
2010
4108
|
: null;
|
|
@@ -2029,7 +4127,7 @@ export const bitrix24Plugin = {
|
|
|
2029
4127
|
chatId: msgCtx.chatInternalId,
|
|
2030
4128
|
})
|
|
2031
4129
|
: null;
|
|
2032
|
-
const agentWatchRules = config.agentMode
|
|
4130
|
+
const agentWatchRules = config.agentMode
|
|
2033
4131
|
? resolveAgentWatchRules({
|
|
2034
4132
|
config,
|
|
2035
4133
|
dialogId: msgCtx.chatId,
|
|
@@ -2039,37 +4137,61 @@ export const bitrix24Plugin = {
|
|
|
2039
4137
|
const watchRule = msgCtx.isGroup && groupAccess?.groupAllowed
|
|
2040
4138
|
? findMatchingWatchRule(msgCtx, groupAccess?.watch)
|
|
2041
4139
|
: undefined;
|
|
2042
|
-
const
|
|
4140
|
+
const botId = String(bot.botId);
|
|
4141
|
+
const ownerBotDmParticipantIds = new Set([msgCtx.senderId, msgCtx.chatId, msgCtx.chatInternalId]
|
|
4142
|
+
.map((value) => String(value ?? '').trim())
|
|
4143
|
+
.filter(Boolean));
|
|
4144
|
+
const isOwnerBotUserDmMirror = Boolean(config.agentMode
|
|
4145
|
+
&& msgCtx.eventScope === 'user'
|
|
4146
|
+
&& msgCtx.isDm
|
|
2043
4147
|
&& webhookOwnerId
|
|
2044
|
-
&&
|
|
4148
|
+
&& ownerBotDmParticipantIds.has(webhookOwnerId)
|
|
4149
|
+
&& ownerBotDmParticipantIds.has(botId));
|
|
4150
|
+
const isOwnerAuthoredMessage = Boolean(webhookOwnerId && msgCtx.senderId === webhookOwnerId);
|
|
4151
|
+
const isCurrentBotAuthoredMessage = msgCtx.senderId === botId;
|
|
4152
|
+
const activeWatchRule = watchRule?.mode === 'notifyOwnerDm'
|
|
4153
|
+
&& (isOwnerAuthoredMessage || isCurrentBotAuthoredMessage)
|
|
2045
4154
|
? undefined
|
|
2046
4155
|
: watchRule;
|
|
2047
|
-
const agentWatchRule = msgCtx
|
|
2048
|
-
|
|
4156
|
+
const agentWatchRule = findMatchingWatchRule(msgCtx, agentWatchRules);
|
|
4157
|
+
const activeAgentWatchRule = agentWatchRule?.mode === 'notifyOwnerDm'
|
|
4158
|
+
&& !isOwnerBotUserDmMirror
|
|
4159
|
+
&& !isOwnerAuthoredMessage
|
|
4160
|
+
&& !isCurrentBotAuthoredMessage
|
|
4161
|
+
? agentWatchRule
|
|
2049
4162
|
: undefined;
|
|
2050
4163
|
/** Shorthand: record message in RAM history for this dialog. */
|
|
2051
4164
|
const recordHistory = (body) => appendMessageToHistory({ historyCache, historyKey, historyLimit, msgCtx, body });
|
|
4165
|
+
if (isOwnerBotUserDmMirror) {
|
|
4166
|
+
logger.debug('Skipping mirrored agent-mode owner DM user event; bot channel will handle it', {
|
|
4167
|
+
senderId: msgCtx.senderId,
|
|
4168
|
+
chatId: msgCtx.chatId,
|
|
4169
|
+
messageId: msgCtx.messageId,
|
|
4170
|
+
eventScope: msgCtx.eventScope,
|
|
4171
|
+
webhookOwnerId,
|
|
4172
|
+
botId,
|
|
4173
|
+
});
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
2052
4176
|
if (msgCtx.eventScope === 'user') {
|
|
2053
|
-
const isBotDialogUserEvent = msgCtx.isDm && msgCtx.chatId === String(bot.botId);
|
|
2054
|
-
const isBotAuthoredUserEvent = msgCtx.senderId === String(bot.botId);
|
|
2055
|
-
if (isBotDialogUserEvent || isBotAuthoredUserEvent) {
|
|
2056
|
-
logger.debug('Skipping agent-mode user event for bot-owned conversation', {
|
|
2057
|
-
senderId: msgCtx.senderId,
|
|
2058
|
-
chatId: msgCtx.chatId,
|
|
2059
|
-
messageId: msgCtx.messageId,
|
|
2060
|
-
isBotDialogUserEvent,
|
|
2061
|
-
isBotAuthoredUserEvent,
|
|
2062
|
-
});
|
|
2063
|
-
return;
|
|
2064
|
-
}
|
|
2065
4177
|
recordHistory();
|
|
2066
|
-
if (
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
4178
|
+
if (activeAgentWatchRule) {
|
|
4179
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
4180
|
+
logger.debug('Skipping duplicate agent watch notification for recent message', {
|
|
4181
|
+
senderId: msgCtx.senderId,
|
|
4182
|
+
chatId: msgCtx.chatId,
|
|
4183
|
+
messageId: msgCtx.messageId,
|
|
4184
|
+
eventScope: msgCtx.eventScope,
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeAgentWatchRule)) {
|
|
4188
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
4189
|
+
logger.debug('User-event watch matched and notified webhook owner in DM', {
|
|
4190
|
+
senderId: msgCtx.senderId,
|
|
4191
|
+
chatId: msgCtx.chatId,
|
|
4192
|
+
messageId: msgCtx.messageId,
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
2073
4195
|
}
|
|
2074
4196
|
return;
|
|
2075
4197
|
}
|
|
@@ -2090,6 +4212,7 @@ export const bitrix24Plugin = {
|
|
|
2090
4212
|
await sendService.markRead(sendCtx, toMessageId(msgCtx.messageId));
|
|
2091
4213
|
if (msgCtx.isGroup
|
|
2092
4214
|
&& !activeWatchRule
|
|
4215
|
+
&& !activeAgentWatchRule
|
|
2093
4216
|
&& groupAccess?.requireMention
|
|
2094
4217
|
&& !msgCtx.wasMentioned) {
|
|
2095
4218
|
recordHistory();
|
|
@@ -2102,12 +4225,42 @@ export const bitrix24Plugin = {
|
|
|
2102
4225
|
}
|
|
2103
4226
|
if (activeWatchRule?.mode === 'notifyOwnerDm') {
|
|
2104
4227
|
recordHistory();
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
4228
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
4229
|
+
logger.debug('Skipping duplicate group watch notification for recent message', {
|
|
4230
|
+
senderId: msgCtx.senderId,
|
|
4231
|
+
chatId: msgCtx.chatId,
|
|
4232
|
+
messageId: msgCtx.messageId,
|
|
4233
|
+
eventScope: msgCtx.eventScope,
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeWatchRule)) {
|
|
4237
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
4238
|
+
logger.debug('Group watch matched and notified webhook owner in DM', {
|
|
4239
|
+
senderId: msgCtx.senderId,
|
|
4240
|
+
chatId: msgCtx.chatId,
|
|
4241
|
+
messageId: msgCtx.messageId,
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
if (activeAgentWatchRule) {
|
|
4247
|
+
recordHistory();
|
|
4248
|
+
if (hasRecentWatchNotification(msgCtx.messageId, ctx.accountId)) {
|
|
4249
|
+
logger.debug('Skipping duplicate bot-scope agent watch notification for recent message', {
|
|
4250
|
+
senderId: msgCtx.senderId,
|
|
4251
|
+
chatId: msgCtx.chatId,
|
|
4252
|
+
messageId: msgCtx.messageId,
|
|
4253
|
+
eventScope: msgCtx.eventScope,
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4256
|
+
else if (await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeAgentWatchRule)) {
|
|
4257
|
+
markRecentWatchNotification(msgCtx.messageId, ctx.accountId);
|
|
4258
|
+
logger.debug('Bot-scope agent watch matched and notified webhook owner in DM', {
|
|
4259
|
+
senderId: msgCtx.senderId,
|
|
4260
|
+
chatId: msgCtx.chatId,
|
|
4261
|
+
messageId: msgCtx.messageId,
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
2111
4264
|
return;
|
|
2112
4265
|
}
|
|
2113
4266
|
const accessResult = activeWatchRule
|
|
@@ -2339,10 +4492,22 @@ export const bitrix24Plugin = {
|
|
|
2339
4492
|
accountId: ctx.accountId,
|
|
2340
4493
|
peer: conversation.peer,
|
|
2341
4494
|
});
|
|
4495
|
+
const inlineButtonsAgentHint = buildBitrix24InlineButtonsAgentHint();
|
|
4496
|
+
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
4497
|
+
const canUseNativeReply = shouldUseBitrix24NativeReply({ isDm });
|
|
4498
|
+
const nativeReplyAgentHint = canUseNativeReply
|
|
4499
|
+
? buildBitrix24NativeReplyAgentHint(commandMessageId)
|
|
4500
|
+
: undefined;
|
|
4501
|
+
const commandBodyForAgent = [
|
|
4502
|
+
inlineButtonsAgentHint,
|
|
4503
|
+
fileDeliveryAgentHint,
|
|
4504
|
+
nativeReplyAgentHint,
|
|
4505
|
+
commandText,
|
|
4506
|
+
].filter(Boolean).join('\n\n');
|
|
2342
4507
|
const slashSessionKey = `bitrix24:slash:${senderId}:${Date.now()}`;
|
|
2343
4508
|
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
2344
4509
|
Body: commandText,
|
|
2345
|
-
BodyForAgent:
|
|
4510
|
+
BodyForAgent: commandBodyForAgent,
|
|
2346
4511
|
RawBody: commandText,
|
|
2347
4512
|
CommandBody: commandText,
|
|
2348
4513
|
CommandAuthorized: true,
|
|
@@ -2380,20 +4545,31 @@ export const bitrix24Plugin = {
|
|
|
2380
4545
|
deliver: async (payload) => {
|
|
2381
4546
|
await replyStatusHeartbeat.stopAndWait();
|
|
2382
4547
|
if (payload.text) {
|
|
4548
|
+
const replyDirective = extractReplyDirective({
|
|
4549
|
+
text: payload.text,
|
|
4550
|
+
currentMessageId: commandMessageId,
|
|
4551
|
+
explicitReplyToId: payload.replyToId,
|
|
4552
|
+
});
|
|
2383
4553
|
const keyboard = extractKeyboardFromPayload(payload)
|
|
2384
4554
|
?? defaultSessionKeyboard;
|
|
2385
4555
|
const formattedPayload = normalizeCommandReplyPayload({
|
|
2386
4556
|
commandName,
|
|
2387
4557
|
commandParams,
|
|
2388
|
-
text:
|
|
4558
|
+
text: replyDirective.cleanText,
|
|
2389
4559
|
language: cmdCtx.language,
|
|
2390
4560
|
});
|
|
4561
|
+
const sendOptions = {
|
|
4562
|
+
keyboard,
|
|
4563
|
+
convertMarkdown: formattedPayload.convertMarkdown,
|
|
4564
|
+
...(canUseNativeReply && replyDirective.replyToMessageId
|
|
4565
|
+
? { replyToMessageId: replyDirective.replyToMessageId }
|
|
4566
|
+
: {}),
|
|
4567
|
+
};
|
|
2391
4568
|
if (!commandReplyDelivered) {
|
|
2392
|
-
if (isDm) {
|
|
4569
|
+
if (isDm || (canUseNativeReply && replyDirective.replyToMessageId)) {
|
|
2393
4570
|
commandReplyDelivered = true;
|
|
2394
4571
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
2395
|
-
|
|
2396
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
4572
|
+
...sendOptions,
|
|
2397
4573
|
});
|
|
2398
4574
|
}
|
|
2399
4575
|
else {
|
|
@@ -2406,10 +4582,7 @@ export const bitrix24Plugin = {
|
|
|
2406
4582
|
return;
|
|
2407
4583
|
}
|
|
2408
4584
|
commandReplyDelivered = true;
|
|
2409
|
-
await sendService.sendText(sendCtx, formattedPayload.text,
|
|
2410
|
-
keyboard,
|
|
2411
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
2412
|
-
});
|
|
4585
|
+
await sendService.sendText(sendCtx, formattedPayload.text, sendOptions);
|
|
2413
4586
|
}
|
|
2414
4587
|
},
|
|
2415
4588
|
onReplyStart: async () => {
|
|
@@ -2420,20 +4593,68 @@ export const bitrix24Plugin = {
|
|
|
2420
4593
|
},
|
|
2421
4594
|
},
|
|
2422
4595
|
});
|
|
4596
|
+
if (!commandReplyDelivered && hasRecentMediaDelivery(sendCtx.dialogId, commandMessageId, ctx.accountId)) {
|
|
4597
|
+
commandReplyDelivered = true;
|
|
4598
|
+
logger.debug('Suppressing command fallback after successful Bitrix24 file upload in the same turn', {
|
|
4599
|
+
commandName,
|
|
4600
|
+
senderId,
|
|
4601
|
+
dialogId,
|
|
4602
|
+
messageId: commandMessageId,
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
if (!commandReplyDelivered && hasRecentSuccessfulAction(commandMessageId, ctx.accountId)) {
|
|
4606
|
+
commandReplyDelivered = true;
|
|
4607
|
+
logger.debug('Suppressing command fallback after successful Bitrix24 action in the same turn', {
|
|
4608
|
+
commandName,
|
|
4609
|
+
senderId,
|
|
4610
|
+
dialogId,
|
|
4611
|
+
messageId: commandMessageId,
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
const shouldWaitForEmptyCommandReplyFallback = !commandReplyDelivered
|
|
4615
|
+
&& isEmptyDispatchResult(dispatchResult);
|
|
2423
4616
|
if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
2424
4617
|
logger.debug('Command reply completed without queued final block, waiting before fallback', {
|
|
2425
4618
|
commandName,
|
|
2426
4619
|
senderId,
|
|
2427
4620
|
dialogId,
|
|
2428
4621
|
counts: dispatchResult.counts,
|
|
4622
|
+
fallbackDelayMs: shouldWaitForEmptyCommandReplyFallback
|
|
4623
|
+
? EMPTY_REPLY_FALLBACK_GRACE_MS
|
|
4624
|
+
: REPLY_FALLBACK_GRACE_MS,
|
|
2429
4625
|
});
|
|
2430
|
-
|
|
4626
|
+
if (shouldWaitForEmptyCommandReplyFallback) {
|
|
4627
|
+
await waitEmptyReplyFallbackGraceWindow();
|
|
4628
|
+
}
|
|
4629
|
+
else {
|
|
4630
|
+
await waitReplyFallbackGraceWindow();
|
|
4631
|
+
}
|
|
2431
4632
|
}
|
|
2432
|
-
if (!commandReplyDelivered && dispatchResult
|
|
4633
|
+
if (!commandReplyDelivered && isEmptyDispatchResult(dispatchResult)) {
|
|
4634
|
+
commandReplyDelivered = true;
|
|
4635
|
+
logger.warn('Command reply completed without any user-visible payload or tool activity; sending fallback notice', {
|
|
4636
|
+
commandName,
|
|
4637
|
+
senderId,
|
|
4638
|
+
dialogId,
|
|
4639
|
+
messageId: commandMessageId,
|
|
4640
|
+
counts: dispatchResult.counts,
|
|
4641
|
+
});
|
|
4642
|
+
const fallbackText = emptyReplyFallback(cmdCtx.language);
|
|
4643
|
+
if (isDm) {
|
|
4644
|
+
await sendService.sendText(sendCtx, fallbackText);
|
|
4645
|
+
}
|
|
4646
|
+
else {
|
|
4647
|
+
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
4648
|
+
keyboard: defaultCommandKeyboard,
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
else if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
2433
4653
|
logger.warn('Command reply completed without a final user-visible message; fallback notice suppressed', {
|
|
2434
4654
|
commandName,
|
|
2435
4655
|
senderId,
|
|
2436
4656
|
dialogId,
|
|
4657
|
+
messageId: commandMessageId,
|
|
2437
4658
|
counts: dispatchResult.counts,
|
|
2438
4659
|
});
|
|
2439
4660
|
}
|
|
@@ -2442,6 +4663,7 @@ export const bitrix24Plugin = {
|
|
|
2442
4663
|
commandName,
|
|
2443
4664
|
senderId,
|
|
2444
4665
|
dialogId,
|
|
4666
|
+
messageId: commandMessageId,
|
|
2445
4667
|
});
|
|
2446
4668
|
}
|
|
2447
4669
|
}
|