@ihazz/bitrix24 1.1.12 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -4
- package/dist/src/api.d.ts +10 -5
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +42 -8
- package/dist/src/api.js.map +1 -1
- package/dist/src/channel.d.ts +18 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +1253 -42
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.js +68 -68
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/inbound-handler.js +85 -7
- package/dist/src/inbound-handler.js.map +1 -1
- package/dist/src/media-service.d.ts +2 -0
- package/dist/src/media-service.d.ts.map +1 -1
- package/dist/src/media-service.js +117 -14
- package/dist/src/media-service.js.map +1 -1
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/message-utils.js +73 -3
- package/dist/src/message-utils.js.map +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/send-service.d.ts +1 -0
- package/dist/src/send-service.d.ts.map +1 -1
- package/dist/src/send-service.js +26 -3
- package/dist/src/send-service.js.map +1 -1
- package/dist/src/state-paths.d.ts +1 -0
- package/dist/src/state-paths.d.ts.map +1 -1
- package/dist/src/state-paths.js +9 -0
- package/dist/src/state-paths.js.map +1 -1
- package/dist/src/types.d.ts +92 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +62 -13
- package/src/channel.ts +1734 -76
- package/src/i18n.ts +68 -68
- package/src/inbound-handler.ts +110 -7
- package/src/media-service.ts +146 -15
- package/src/message-utils.ts +90 -3
- package/src/runtime.ts +1 -0
- package/src/send-service.ts +40 -2
- package/src/state-paths.ts +11 -0
- package/src/types.ts +122 -0
package/dist/src/channel.js
CHANGED
|
@@ -16,6 +16,7 @@ import { getBitrix24Runtime } from './runtime.js';
|
|
|
16
16
|
import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply, getCommandRegistrationPayload, } from './commands.js';
|
|
17
17
|
import { accessApproved, accessDenied, commandKeyboardLabels, 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;
|
|
@@ -37,7 +38,15 @@ const FORWARDED_CONTEXT_RANGE = 5;
|
|
|
37
38
|
const ACTIVE_SESSION_NAMESPACE_TTL_MS = 180 * 24 * 60 * 60 * 1000;
|
|
38
39
|
const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
|
|
39
40
|
const ACTIVE_SESSION_NAMESPACE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
41
|
+
const NATIVE_FORWARD_ACK_TTL_MS = 60 * 1000;
|
|
42
|
+
const NATIVE_REACTION_ACK_TTL_MS = 60 * 1000;
|
|
43
|
+
const RECENT_INBOUND_MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
44
|
+
const RECENT_INBOUND_MESSAGE_LIMIT = 20;
|
|
40
45
|
const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
|
|
46
|
+
const pendingNativeForwardAcks = new Map();
|
|
47
|
+
const pendingNativeReactionAcks = new Map();
|
|
48
|
+
const recentInboundMessagesByDialog = new Map();
|
|
49
|
+
const inboundMessageContextById = new Map();
|
|
41
50
|
// ─── Emoji → B24 reaction code mapping ──────────────────────────────────
|
|
42
51
|
// B24 uses named reaction codes, not Unicode emoji.
|
|
43
52
|
// Map common Unicode emoji to their B24 equivalents.
|
|
@@ -109,6 +118,424 @@ function toMessageId(value) {
|
|
|
109
118
|
const parsed = Number(value);
|
|
110
119
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
111
120
|
}
|
|
121
|
+
function resolveStateAccountId(accountId) {
|
|
122
|
+
const normalizedAccountId = typeof accountId === 'string'
|
|
123
|
+
? accountId.trim()
|
|
124
|
+
: '';
|
|
125
|
+
if (normalizedAccountId) {
|
|
126
|
+
return normalizedAccountId;
|
|
127
|
+
}
|
|
128
|
+
const gatewayAccountId = gatewayState?.accountId?.trim();
|
|
129
|
+
return gatewayAccountId || 'default';
|
|
130
|
+
}
|
|
131
|
+
function buildAccountDialogScopeKey(accountId, dialogId) {
|
|
132
|
+
const normalizedDialogId = dialogId.trim();
|
|
133
|
+
if (!normalizedDialogId) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return `${resolveStateAccountId(accountId)}:${normalizedDialogId}`;
|
|
137
|
+
}
|
|
138
|
+
function buildAccountMessageScopeKey(accountId, currentMessageId) {
|
|
139
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
140
|
+
if (!normalizedMessageId) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return `${resolveStateAccountId(accountId)}:${normalizedMessageId}`;
|
|
144
|
+
}
|
|
145
|
+
function buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId) {
|
|
146
|
+
const dialogKey = buildAccountDialogScopeKey(accountId, dialogId);
|
|
147
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
148
|
+
if (!dialogKey || !normalizedMessageId) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return `${dialogKey}:${normalizedMessageId}`;
|
|
152
|
+
}
|
|
153
|
+
function prunePendingNativeForwardAcks(now = Date.now()) {
|
|
154
|
+
for (const [key, expiresAt] of pendingNativeForwardAcks.entries()) {
|
|
155
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
156
|
+
pendingNativeForwardAcks.delete(key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function markPendingNativeForwardAck(dialogId, currentMessageId, accountId) {
|
|
161
|
+
const key = buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
162
|
+
if (!key) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
prunePendingNativeForwardAcks();
|
|
166
|
+
pendingNativeForwardAcks.set(key, Date.now() + NATIVE_FORWARD_ACK_TTL_MS);
|
|
167
|
+
}
|
|
168
|
+
function consumePendingNativeForwardAck(dialogId, currentMessageId, accountId) {
|
|
169
|
+
const key = buildPendingNativeForwardAckKey(dialogId, currentMessageId, accountId);
|
|
170
|
+
if (!key) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
prunePendingNativeForwardAcks();
|
|
174
|
+
const expiresAt = pendingNativeForwardAcks.get(key);
|
|
175
|
+
if (!expiresAt) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
pendingNativeForwardAcks.delete(key);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function resolvePendingNativeForwardAckDialogId(currentMessageId, fallbackDialogId, accountId) {
|
|
182
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
183
|
+
if (!messageKey) {
|
|
184
|
+
return fallbackDialogId;
|
|
185
|
+
}
|
|
186
|
+
return inboundMessageContextById.get(messageKey)?.dialogId ?? fallbackDialogId;
|
|
187
|
+
}
|
|
188
|
+
function buildPendingNativeReactionAckKey(currentMessageId, accountId) {
|
|
189
|
+
return buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
190
|
+
}
|
|
191
|
+
function prunePendingNativeReactionAcks(now = Date.now()) {
|
|
192
|
+
for (const [key, entry] of pendingNativeReactionAcks.entries()) {
|
|
193
|
+
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
|
194
|
+
pendingNativeReactionAcks.delete(key);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function markPendingNativeReactionAck(currentMessageId, emoji, accountId) {
|
|
199
|
+
const key = buildPendingNativeReactionAckKey(currentMessageId, accountId);
|
|
200
|
+
const normalizedEmoji = emoji.trim();
|
|
201
|
+
if (!key || !normalizedEmoji) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
prunePendingNativeReactionAcks();
|
|
205
|
+
pendingNativeReactionAcks.set(key, {
|
|
206
|
+
emoji: normalizedEmoji,
|
|
207
|
+
expiresAt: Date.now() + NATIVE_REACTION_ACK_TTL_MS,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function consumePendingNativeReactionAck(currentMessageId, text, accountId) {
|
|
211
|
+
const key = buildPendingNativeReactionAckKey(currentMessageId, accountId);
|
|
212
|
+
if (!key) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
prunePendingNativeReactionAcks();
|
|
216
|
+
const pendingAck = pendingNativeReactionAcks.get(key);
|
|
217
|
+
if (!pendingAck) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
if (normalizeComparableMessageText(text) !== pendingAck.emoji) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
pendingNativeReactionAcks.delete(key);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
function pruneRecentInboundMessages(now = Date.now()) {
|
|
227
|
+
for (const [dialogKey, entries] of recentInboundMessagesByDialog.entries()) {
|
|
228
|
+
const nextEntries = entries.filter((entry) => Number.isFinite(entry.timestamp) && entry.timestamp > now - RECENT_INBOUND_MESSAGE_TTL_MS);
|
|
229
|
+
if (nextEntries.length === 0) {
|
|
230
|
+
recentInboundMessagesByDialog.delete(dialogKey);
|
|
231
|
+
}
|
|
232
|
+
else if (nextEntries.length !== entries.length) {
|
|
233
|
+
recentInboundMessagesByDialog.set(dialogKey, nextEntries);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const [messageId, entry] of inboundMessageContextById.entries()) {
|
|
237
|
+
if (!Number.isFinite(entry.timestamp) || entry.timestamp <= now - RECENT_INBOUND_MESSAGE_TTL_MS) {
|
|
238
|
+
inboundMessageContextById.delete(messageId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function rememberRecentInboundMessage(dialogId, messageId, body, timestamp = Date.now(), accountId) {
|
|
243
|
+
const normalizedMessageId = toMessageId(messageId);
|
|
244
|
+
const normalizedBody = body.trim();
|
|
245
|
+
const dialogKey = buildAccountDialogScopeKey(accountId, dialogId);
|
|
246
|
+
const messageKey = buildAccountMessageScopeKey(accountId, normalizedMessageId);
|
|
247
|
+
if (!dialogKey || !messageKey || !normalizedBody) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
pruneRecentInboundMessages(timestamp);
|
|
251
|
+
const id = String(normalizedMessageId);
|
|
252
|
+
const currentEntries = recentInboundMessagesByDialog.get(dialogKey) ?? [];
|
|
253
|
+
const nextEntries = currentEntries
|
|
254
|
+
.filter((entry) => entry.messageId !== id)
|
|
255
|
+
.concat({ messageId: id, body: normalizedBody, timestamp })
|
|
256
|
+
.slice(-RECENT_INBOUND_MESSAGE_LIMIT);
|
|
257
|
+
recentInboundMessagesByDialog.set(dialogKey, nextEntries);
|
|
258
|
+
inboundMessageContextById.set(messageKey, {
|
|
259
|
+
dialogId,
|
|
260
|
+
dialogKey,
|
|
261
|
+
body: normalizedBody,
|
|
262
|
+
timestamp,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function findPreviousRecentInboundMessage(currentMessageId, accountId) {
|
|
266
|
+
const messageKey = buildAccountMessageScopeKey(accountId, currentMessageId);
|
|
267
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
268
|
+
if (!messageKey || !normalizedMessageId) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
pruneRecentInboundMessages();
|
|
272
|
+
const currentEntry = inboundMessageContextById.get(messageKey);
|
|
273
|
+
if (!currentEntry) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const dialogEntries = recentInboundMessagesByDialog.get(currentEntry.dialogKey);
|
|
277
|
+
if (!dialogEntries || dialogEntries.length === 0) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const currentIndex = dialogEntries.findIndex((entry) => entry.messageId === String(normalizedMessageId));
|
|
281
|
+
if (currentIndex <= 0) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
for (let index = currentIndex - 1; index >= 0; index -= 1) {
|
|
285
|
+
const entry = dialogEntries[index];
|
|
286
|
+
if (entry.body.trim()) {
|
|
287
|
+
return entry;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
const INLINE_REPLY_DIRECTIVE_RE = /\[\[\s*(reply_to_current|reply_to\s*:\s*([^\]\n]+))\s*\]\]/gi;
|
|
293
|
+
function normalizeComparableMessageText(value) {
|
|
294
|
+
return value
|
|
295
|
+
.replace(/\s+/g, ' ')
|
|
296
|
+
.trim();
|
|
297
|
+
}
|
|
298
|
+
function normalizeBitrix24DialogTarget(raw) {
|
|
299
|
+
const stripped = raw.trim().replace(CHANNEL_PREFIX_RE, '');
|
|
300
|
+
if (!stripped) {
|
|
301
|
+
return '';
|
|
302
|
+
}
|
|
303
|
+
const bracketedUserMatch = stripped.match(/^\[USER=(\d+)(?:[^\]]*)\][\s\S]*?\[\/USER\]$/iu);
|
|
304
|
+
if (bracketedUserMatch?.[1]) {
|
|
305
|
+
return bracketedUserMatch[1];
|
|
306
|
+
}
|
|
307
|
+
const bracketedChatMatch = stripped.match(/^\[CHAT=(?:chat)?(\d+)(?:[^\]]*)\][\s\S]*?\[\/CHAT\]$/iu);
|
|
308
|
+
if (bracketedChatMatch?.[1]) {
|
|
309
|
+
return `chat${bracketedChatMatch[1]}`;
|
|
310
|
+
}
|
|
311
|
+
const plainUserMatch = stripped.match(/^USER=(\d+)$/iu);
|
|
312
|
+
if (plainUserMatch?.[1]) {
|
|
313
|
+
return plainUserMatch[1];
|
|
314
|
+
}
|
|
315
|
+
const plainChatMatch = stripped.match(/^CHAT=(?:chat)?(\d+)$/iu);
|
|
316
|
+
if (plainChatMatch?.[1]) {
|
|
317
|
+
return `chat${plainChatMatch[1]}`;
|
|
318
|
+
}
|
|
319
|
+
const prefixedChatMatch = stripped.match(/^chat(\d+)$/iu);
|
|
320
|
+
if (prefixedChatMatch?.[1]) {
|
|
321
|
+
return `chat${prefixedChatMatch[1]}`;
|
|
322
|
+
}
|
|
323
|
+
return stripped;
|
|
324
|
+
}
|
|
325
|
+
function looksLikeBitrix24DialogTarget(raw) {
|
|
326
|
+
const normalized = normalizeBitrix24DialogTarget(raw);
|
|
327
|
+
return /^\d+$/.test(normalized) || /^chat\d+$/iu.test(normalized);
|
|
328
|
+
}
|
|
329
|
+
function extractBitrix24DialogTargetsFromText(text) {
|
|
330
|
+
const targets = new Set();
|
|
331
|
+
for (const match of text.matchAll(/\[CHAT=(?:chat)?(\d+)(?:[^\]]*)\][\s\S]*?\[\/CHAT\]/giu)) {
|
|
332
|
+
if (match[1]) {
|
|
333
|
+
targets.add(`chat${match[1]}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const match of text.matchAll(/\[USER=(\d+)(?:[^\]]*)\][\s\S]*?\[\/USER\]/giu)) {
|
|
337
|
+
if (match[1]) {
|
|
338
|
+
targets.add(match[1]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const match of text.matchAll(/\bCHAT=(?:chat)?(\d+)\b/giu)) {
|
|
342
|
+
if (match[1]) {
|
|
343
|
+
targets.add(`chat${match[1]}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const match of text.matchAll(/\bUSER=(\d+)\b/giu)) {
|
|
347
|
+
if (match[1]) {
|
|
348
|
+
targets.add(match[1]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return [...targets];
|
|
352
|
+
}
|
|
353
|
+
function isLikelyForwardControlMessage(text) {
|
|
354
|
+
return extractBitrix24DialogTargetsFromText(text).length > 0;
|
|
355
|
+
}
|
|
356
|
+
function extractReplyDirective(params) {
|
|
357
|
+
let wantsCurrentReply = false;
|
|
358
|
+
let inlineReplyToId;
|
|
359
|
+
const cleanText = cleanupTextAfterInlineMediaExtraction(params.text.replace(INLINE_REPLY_DIRECTIVE_RE, (_match, _rawDirective, explicitId) => {
|
|
360
|
+
if (typeof explicitId === 'string' && explicitId.trim()) {
|
|
361
|
+
inlineReplyToId = explicitId.trim();
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
wantsCurrentReply = true;
|
|
365
|
+
}
|
|
366
|
+
return ' ';
|
|
367
|
+
}));
|
|
368
|
+
const explicitReplyToMessageId = parseActionMessageIds(params.explicitReplyToId)[0];
|
|
369
|
+
if (explicitReplyToMessageId) {
|
|
370
|
+
return { cleanText, replyToMessageId: explicitReplyToMessageId };
|
|
371
|
+
}
|
|
372
|
+
const inlineReplyToMessageId = parseActionMessageIds(inlineReplyToId)[0];
|
|
373
|
+
if (inlineReplyToMessageId) {
|
|
374
|
+
return { cleanText, replyToMessageId: inlineReplyToMessageId };
|
|
375
|
+
}
|
|
376
|
+
if (wantsCurrentReply) {
|
|
377
|
+
const currentReplyToMessageId = parseActionMessageIds(params.currentMessageId)[0];
|
|
378
|
+
if (currentReplyToMessageId) {
|
|
379
|
+
return { cleanText, replyToMessageId: currentReplyToMessageId };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { cleanText };
|
|
383
|
+
}
|
|
384
|
+
function findNativeForwardTarget(params) {
|
|
385
|
+
const deliveredText = normalizeComparableMessageText(params.deliveredText);
|
|
386
|
+
if (!deliveredText) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
if (deliveredText === normalizeComparableMessageText(params.requestText)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
const currentMessageId = toMessageId(params.currentMessageId);
|
|
393
|
+
const matchedEntry = [...params.historyEntries]
|
|
394
|
+
.reverse()
|
|
395
|
+
.find((entry) => {
|
|
396
|
+
if (normalizeComparableMessageText(entry.body) !== deliveredText) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
return currentMessageId === undefined || toMessageId(entry.messageId) !== currentMessageId;
|
|
400
|
+
});
|
|
401
|
+
if (!matchedEntry) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
return toMessageId(matchedEntry.messageId);
|
|
405
|
+
}
|
|
406
|
+
function findImplicitForwardTargetFromRecentInbound(params) {
|
|
407
|
+
const normalizedDeliveredText = normalizeComparableMessageText(params.deliveredText);
|
|
408
|
+
const normalizedRequestText = normalizeComparableMessageText(params.requestText);
|
|
409
|
+
const targetDialogIds = extractBitrix24DialogTargetsFromText(params.requestText);
|
|
410
|
+
const isForwardMarkerOnly = Boolean(normalizedDeliveredText) && FORWARD_MARKER_RE.test(normalizedDeliveredText);
|
|
411
|
+
if (!normalizedDeliveredText
|
|
412
|
+
|| (!isForwardMarkerOnly
|
|
413
|
+
&& (targetDialogIds.length === 0
|
|
414
|
+
|| normalizedDeliveredText !== normalizedRequestText))) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(params.currentMessageId, params.accountId);
|
|
418
|
+
if (previousInboundMessage) {
|
|
419
|
+
return Number(previousInboundMessage.messageId);
|
|
420
|
+
}
|
|
421
|
+
const previousHistoryEntry = [...(params.historyEntries ?? [])]
|
|
422
|
+
.reverse()
|
|
423
|
+
.find((entry) => toMessageId(entry.messageId));
|
|
424
|
+
return toMessageId(previousHistoryEntry?.messageId);
|
|
425
|
+
}
|
|
426
|
+
function findPreviousForwardableMessageId(params) {
|
|
427
|
+
// If the user replied to a specific message, that's the source to forward
|
|
428
|
+
const replyToId = toMessageId(params.replyToMessageId);
|
|
429
|
+
if (replyToId) {
|
|
430
|
+
return replyToId;
|
|
431
|
+
}
|
|
432
|
+
const normalizedCurrentBody = normalizeComparableMessageText(params.currentBody);
|
|
433
|
+
const previousHistoryEntry = [...(params.historyEntries ?? [])]
|
|
434
|
+
.reverse()
|
|
435
|
+
.find((entry) => {
|
|
436
|
+
const messageId = toMessageId(entry.messageId);
|
|
437
|
+
const normalizedBody = normalizeComparableMessageText(entry.body);
|
|
438
|
+
if (!messageId || !normalizedBody) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (normalizedCurrentBody && normalizedBody === normalizedCurrentBody) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return !isLikelyForwardControlMessage(entry.body);
|
|
445
|
+
});
|
|
446
|
+
if (previousHistoryEntry) {
|
|
447
|
+
return toMessageId(previousHistoryEntry.messageId);
|
|
448
|
+
}
|
|
449
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(params.currentMessageId, params.accountId);
|
|
450
|
+
if (previousInboundMessage) {
|
|
451
|
+
if (normalizeComparableMessageText(previousInboundMessage.body) !== normalizedCurrentBody
|
|
452
|
+
&& !isLikelyForwardControlMessage(previousInboundMessage.body)) {
|
|
453
|
+
return Number(previousInboundMessage.messageId);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
function buildBitrix24NativeForwardAgentHint(params) {
|
|
459
|
+
const targetDialogIds = extractBitrix24DialogTargetsFromText(params.currentBody);
|
|
460
|
+
const sourceMessageId = findPreviousForwardableMessageId(params);
|
|
461
|
+
if (!sourceMessageId) {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
const formattedTargets = targetDialogIds
|
|
465
|
+
.map((dialogId) => (dialogId.startsWith('chat') ? `${dialogId} (chat)` : `${dialogId} (user)`))
|
|
466
|
+
.join(', ');
|
|
467
|
+
const lines = [
|
|
468
|
+
'[Bitrix24 native forward instruction]',
|
|
469
|
+
'Use this only if you intentionally want a native Bitrix24 forward instead of repeating visible text.',
|
|
470
|
+
`Previous Bitrix24 message id available for forwarding: ${sourceMessageId}.`,
|
|
471
|
+
'Use action "send" with "forwardMessageIds" set to that source message id.',
|
|
472
|
+
'If the tool requires a non-empty message field for a pure forward, use "↩️".',
|
|
473
|
+
'Do not repeat the source message text or the user instruction text when doing a native forward.',
|
|
474
|
+
];
|
|
475
|
+
if (formattedTargets) {
|
|
476
|
+
lines.push(`Explicit target dialog ids mentioned in the current message: ${formattedTargets}.`);
|
|
477
|
+
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.');
|
|
478
|
+
lines.push('Do not use CHAT=xxx or USER=xxx format for "to" — use "chat520" or "77" directly.');
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
lines.push('If you forward in the current dialog, do not set "to" to another dialog id.');
|
|
482
|
+
}
|
|
483
|
+
lines.push('[/Bitrix24 native forward instruction]');
|
|
484
|
+
return lines.join('\n');
|
|
485
|
+
}
|
|
486
|
+
function buildBitrix24NativeReplyAgentHint(currentMessageId) {
|
|
487
|
+
const normalizedMessageId = toMessageId(currentMessageId);
|
|
488
|
+
if (!normalizedMessageId) {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
return [
|
|
492
|
+
'[Bitrix24 native reply instruction]',
|
|
493
|
+
`Current Bitrix24 message id: ${normalizedMessageId}.`,
|
|
494
|
+
'If you intentionally want a native Bitrix24 reply to the current inbound message, set "replyToMessageId" to that id.',
|
|
495
|
+
'For text-only payloads you can also prepend [[reply_to_current]] to the reply text.',
|
|
496
|
+
'Do not use a native reply unless you actually want reply threading.',
|
|
497
|
+
'[/Bitrix24 native reply instruction]',
|
|
498
|
+
].join('\n');
|
|
499
|
+
}
|
|
500
|
+
function buildBitrix24FileDeliveryAgentHint() {
|
|
501
|
+
return [
|
|
502
|
+
'[Bitrix24 file delivery instruction]',
|
|
503
|
+
'When you need to deliver a real file or document to the user, use structured reply payload field "mediaUrl" or "mediaUrls".',
|
|
504
|
+
'Set "mediaUrl" to the local path of the generated file in the OpenClaw workspace or managed media directory.',
|
|
505
|
+
'Use "text" only for an optional short caption.',
|
|
506
|
+
'Do not place local file paths inside markdown links or plain text, because that only sends text to the user and does not upload the file.',
|
|
507
|
+
'[/Bitrix24 file delivery instruction]',
|
|
508
|
+
].join('\n');
|
|
509
|
+
}
|
|
510
|
+
function readExplicitForwardMessageIds(rawParams) {
|
|
511
|
+
return parseActionMessageIds(rawParams.forwardMessageIds
|
|
512
|
+
?? rawParams.forwardIds);
|
|
513
|
+
}
|
|
514
|
+
function resolveActionForwardMessageIds(params) {
|
|
515
|
+
const explicitForwardMessages = readExplicitForwardMessageIds(params.rawParams);
|
|
516
|
+
if (explicitForwardMessages.length > 0) {
|
|
517
|
+
return explicitForwardMessages;
|
|
518
|
+
}
|
|
519
|
+
const aliasedForwardMessages = parseActionMessageIds(params.rawParams.messageIds
|
|
520
|
+
?? params.rawParams.message_ids
|
|
521
|
+
?? params.rawParams.messageId
|
|
522
|
+
?? params.rawParams.message_id
|
|
523
|
+
?? params.rawParams.replyToMessageId
|
|
524
|
+
?? params.rawParams.replyToId);
|
|
525
|
+
if (aliasedForwardMessages.length > 0) {
|
|
526
|
+
return aliasedForwardMessages;
|
|
527
|
+
}
|
|
528
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(params.toolContext?.currentMessageId, params.accountId);
|
|
529
|
+
return previousInboundMessage ? [Number(previousInboundMessage.messageId)] : [];
|
|
530
|
+
}
|
|
531
|
+
const FORWARD_MARKER_RE = /^[\s↩️➡️→⤵️🔄📨]+$/u;
|
|
532
|
+
function normalizeForwardActionText(text) {
|
|
533
|
+
const normalizedText = typeof text === 'string' ? text.trim() : '';
|
|
534
|
+
if (!normalizedText || FORWARD_MARKER_RE.test(normalizedText)) {
|
|
535
|
+
return '';
|
|
536
|
+
}
|
|
537
|
+
return normalizedText;
|
|
538
|
+
}
|
|
112
539
|
function escapeBbCodeText(value) {
|
|
113
540
|
return value
|
|
114
541
|
.replace(/\[/g, '(')
|
|
@@ -741,6 +1168,15 @@ class BufferedDirectMessageCoalescer {
|
|
|
741
1168
|
let gatewayState = null;
|
|
742
1169
|
export function __setGatewayStateForTests(state) {
|
|
743
1170
|
gatewayState = state;
|
|
1171
|
+
if (state === null) {
|
|
1172
|
+
pendingNativeForwardAcks.clear();
|
|
1173
|
+
pendingNativeReactionAcks.clear();
|
|
1174
|
+
recentInboundMessagesByDialog.clear();
|
|
1175
|
+
inboundMessageContextById.clear();
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
export function __rememberRecentInboundMessageForTests(params) {
|
|
1179
|
+
rememberRecentInboundMessage(params.dialogId, params.messageId, params.body, params.timestamp, params.accountId);
|
|
744
1180
|
}
|
|
745
1181
|
// ─── Keyboard layouts ────────────────────────────────────────────────────────
|
|
746
1182
|
export function buildWelcomeKeyboard(language) {
|
|
@@ -768,6 +1204,341 @@ export function buildDefaultCommandKeyboard(language) {
|
|
|
768
1204
|
}
|
|
769
1205
|
export const DEFAULT_COMMAND_KEYBOARD = buildDefaultCommandKeyboard();
|
|
770
1206
|
export const DEFAULT_WELCOME_KEYBOARD = buildWelcomeKeyboard();
|
|
1207
|
+
const BITRIX24_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'delete'];
|
|
1208
|
+
const BITRIX24_DISCOVERY_ACTION_NAMES = ['send', 'reply', 'react', 'edit', 'delete'];
|
|
1209
|
+
function buildMessageToolButtonsSchema() {
|
|
1210
|
+
return {
|
|
1211
|
+
type: 'array',
|
|
1212
|
+
description: 'Optional Bitrix24 message buttons as rows of button objects.',
|
|
1213
|
+
items: {
|
|
1214
|
+
type: 'array',
|
|
1215
|
+
items: {
|
|
1216
|
+
type: 'object',
|
|
1217
|
+
additionalProperties: false,
|
|
1218
|
+
properties: {
|
|
1219
|
+
text: {
|
|
1220
|
+
type: 'string',
|
|
1221
|
+
description: 'Visible button label.',
|
|
1222
|
+
},
|
|
1223
|
+
callback_data: {
|
|
1224
|
+
type: 'string',
|
|
1225
|
+
description: 'Registered command like /help, or any plain text payload to send when tapped.',
|
|
1226
|
+
},
|
|
1227
|
+
style: {
|
|
1228
|
+
type: 'string',
|
|
1229
|
+
enum: ['primary', 'attention', 'danger'],
|
|
1230
|
+
description: 'Optional Bitrix24 button accent.',
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
required: ['text'],
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function buildMessageToolAttachSchema() {
|
|
1239
|
+
const blockSchema = {
|
|
1240
|
+
type: 'object',
|
|
1241
|
+
description: 'Single Bitrix24 ATTACH rich block. Supported top-level keys are MESSAGE, LINK, IMAGE, FILE, DELIMITER, GRID or USER.',
|
|
1242
|
+
};
|
|
1243
|
+
return {
|
|
1244
|
+
description: 'Bitrix24 ATTACH rich layout blocks. Use this for rich cards, not for binary file uploads. For a status-owner-link card, prefer blocks like USER, LINK and GRID. Example: [{"USER":{"NAME":"Alice"}},{"LINK":{"NAME":"Open","LINK":"https://example.com"}},{"GRID":[{"NAME":"Status","VALUE":"In progress","DISPLAY":"LINE"},{"NAME":"Owner","VALUE":"Alice","DISPLAY":"LINE"}]}]. Use mediaUrl/mediaUrls for actual files and media.',
|
|
1245
|
+
anyOf: [
|
|
1246
|
+
{
|
|
1247
|
+
type: 'object',
|
|
1248
|
+
additionalProperties: false,
|
|
1249
|
+
properties: {
|
|
1250
|
+
ID: { type: 'integer' },
|
|
1251
|
+
COLOR_TOKEN: {
|
|
1252
|
+
type: 'string',
|
|
1253
|
+
enum: ['primary', 'secondary', 'alert', 'base'],
|
|
1254
|
+
},
|
|
1255
|
+
COLOR: { type: 'string' },
|
|
1256
|
+
BLOCKS: {
|
|
1257
|
+
type: 'array',
|
|
1258
|
+
items: blockSchema,
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
required: ['BLOCKS'],
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
type: 'array',
|
|
1265
|
+
items: blockSchema,
|
|
1266
|
+
},
|
|
1267
|
+
],
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function buildMessageToolIdListSchema(description) {
|
|
1271
|
+
return {
|
|
1272
|
+
anyOf: [
|
|
1273
|
+
{
|
|
1274
|
+
type: 'integer',
|
|
1275
|
+
description,
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
type: 'string',
|
|
1279
|
+
description: `${description} Can also be a comma-separated list or JSON array string.`,
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
type: 'array',
|
|
1283
|
+
description,
|
|
1284
|
+
items: {
|
|
1285
|
+
type: 'integer',
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1288
|
+
],
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function resolveConfiguredBitrix24ActionAccounts(cfg, accountId) {
|
|
1292
|
+
if (accountId) {
|
|
1293
|
+
const account = resolveAccount(cfg, accountId);
|
|
1294
|
+
return account.enabled && account.configured ? [account.accountId] : [];
|
|
1295
|
+
}
|
|
1296
|
+
return listAccountIds(cfg).filter((candidateId) => {
|
|
1297
|
+
const account = resolveAccount(cfg, candidateId);
|
|
1298
|
+
return account.enabled && account.configured;
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
function describeBitrix24MessageTool(params) {
|
|
1302
|
+
const configuredAccounts = resolveConfiguredBitrix24ActionAccounts(params.cfg, params.accountId);
|
|
1303
|
+
if (configuredAccounts.length === 0) {
|
|
1304
|
+
return {
|
|
1305
|
+
actions: [],
|
|
1306
|
+
capabilities: [],
|
|
1307
|
+
schema: null,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
return {
|
|
1311
|
+
actions: [...BITRIX24_DISCOVERY_ACTION_NAMES],
|
|
1312
|
+
capabilities: ['interactive', 'buttons', 'cards'],
|
|
1313
|
+
schema: [
|
|
1314
|
+
{
|
|
1315
|
+
properties: {
|
|
1316
|
+
buttons: buildMessageToolButtonsSchema(),
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
properties: {
|
|
1321
|
+
attach: buildMessageToolAttachSchema(),
|
|
1322
|
+
forwardMessageIds: buildMessageToolIdListSchema('Bitrix24 source message id(s) to forward natively. Use with action="send" plus to/target. Prefer the referenced message id, not the current instruction message id. The message field must be non-empty — use "↩️" (emoji) for a pure forward without extra text.'),
|
|
1323
|
+
forwardIds: buildMessageToolIdListSchema('Alias for forwardMessageIds. Use with action="send".'),
|
|
1324
|
+
replyToId: buildMessageToolIdListSchema('Bitrix24 message id to reply to natively inside the current dialog.'),
|
|
1325
|
+
replyToMessageId: buildMessageToolIdListSchema('Alias for replyToId.'),
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
],
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
function extractBitrix24ToolSend(args) {
|
|
1332
|
+
if ((typeof args.action === 'string' ? args.action.trim() : '') !== 'sendMessage') {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
const to = typeof args.to === 'string' ? normalizeBitrix24DialogTarget(args.to) : '';
|
|
1336
|
+
if (!to) {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
const accountId = typeof args.accountId === 'string'
|
|
1340
|
+
? args.accountId.trim()
|
|
1341
|
+
: undefined;
|
|
1342
|
+
const threadId = typeof args.threadId === 'number'
|
|
1343
|
+
? String(args.threadId)
|
|
1344
|
+
: typeof args.threadId === 'string'
|
|
1345
|
+
? args.threadId.trim()
|
|
1346
|
+
: '';
|
|
1347
|
+
return {
|
|
1348
|
+
to,
|
|
1349
|
+
...(accountId ? { accountId } : {}),
|
|
1350
|
+
...(threadId ? { threadId } : {}),
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
function readActionTextParam(params) {
|
|
1354
|
+
const rawText = params.message ?? params.text ?? params.content;
|
|
1355
|
+
return typeof rawText === 'string' ? rawText : null;
|
|
1356
|
+
}
|
|
1357
|
+
function isPlainObject(value) {
|
|
1358
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1359
|
+
}
|
|
1360
|
+
function isAttachColorToken(value) {
|
|
1361
|
+
return value === 'primary'
|
|
1362
|
+
|| value === 'secondary'
|
|
1363
|
+
|| value === 'alert'
|
|
1364
|
+
|| value === 'base';
|
|
1365
|
+
}
|
|
1366
|
+
function isAttachBlock(value) {
|
|
1367
|
+
return isPlainObject(value) && Object.keys(value).length > 0;
|
|
1368
|
+
}
|
|
1369
|
+
function isBitrix24Attach(value) {
|
|
1370
|
+
if (Array.isArray(value)) {
|
|
1371
|
+
return value.length > 0 && value.every(isAttachBlock);
|
|
1372
|
+
}
|
|
1373
|
+
if (!isPlainObject(value) || !Array.isArray(value.BLOCKS) || value.BLOCKS.length === 0) {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
if (value.COLOR_TOKEN !== undefined && !isAttachColorToken(value.COLOR_TOKEN)) {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
return value.BLOCKS.every(isAttachBlock);
|
|
1380
|
+
}
|
|
1381
|
+
function parseActionAttach(rawValue) {
|
|
1382
|
+
if (rawValue == null) {
|
|
1383
|
+
return undefined;
|
|
1384
|
+
}
|
|
1385
|
+
let parsed = rawValue;
|
|
1386
|
+
if (typeof rawValue === 'string') {
|
|
1387
|
+
const trimmed = rawValue.trim();
|
|
1388
|
+
if (!trimmed) {
|
|
1389
|
+
return undefined;
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
parsed = JSON.parse(trimmed);
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
return undefined;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return isBitrix24Attach(parsed) ? parsed : undefined;
|
|
1399
|
+
}
|
|
1400
|
+
function hasMeaningfulAttachInput(rawValue) {
|
|
1401
|
+
if (rawValue == null) {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
if (typeof rawValue === 'string') {
|
|
1405
|
+
const trimmed = rawValue.trim();
|
|
1406
|
+
if (!trimmed) {
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
try {
|
|
1410
|
+
return hasMeaningfulAttachInput(JSON.parse(trimmed));
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (Array.isArray(rawValue)) {
|
|
1417
|
+
return rawValue.length > 0;
|
|
1418
|
+
}
|
|
1419
|
+
if (isPlainObject(rawValue)) {
|
|
1420
|
+
if (Array.isArray(rawValue.BLOCKS)) {
|
|
1421
|
+
return rawValue.BLOCKS.length > 0;
|
|
1422
|
+
}
|
|
1423
|
+
return Object.keys(rawValue).length > 0;
|
|
1424
|
+
}
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
function readActionTargetParam(params, fallbackTarget) {
|
|
1428
|
+
const rawTarget = params.to ?? params.target ?? fallbackTarget;
|
|
1429
|
+
return typeof rawTarget === 'string' ? normalizeBitrix24DialogTarget(rawTarget) : '';
|
|
1430
|
+
}
|
|
1431
|
+
function parseActionMessageIds(rawValue) {
|
|
1432
|
+
const ids = [];
|
|
1433
|
+
const seen = new Set();
|
|
1434
|
+
const append = (value) => {
|
|
1435
|
+
if (Array.isArray(value)) {
|
|
1436
|
+
value.forEach(append);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (typeof value === 'number') {
|
|
1440
|
+
const normalizedId = Math.trunc(value);
|
|
1441
|
+
if (Number.isFinite(normalizedId) && normalizedId > 0 && !seen.has(normalizedId)) {
|
|
1442
|
+
seen.add(normalizedId);
|
|
1443
|
+
ids.push(normalizedId);
|
|
1444
|
+
}
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (typeof value !== 'string') {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const trimmed = value.trim();
|
|
1451
|
+
if (!trimmed) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (trimmed.startsWith('[')) {
|
|
1455
|
+
try {
|
|
1456
|
+
append(JSON.parse(trimmed));
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
// Fall through to scalar / CSV parsing.
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
trimmed
|
|
1464
|
+
.split(/[,\s]+/)
|
|
1465
|
+
.forEach((part) => {
|
|
1466
|
+
if (!part) {
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const normalizedId = Number(part);
|
|
1470
|
+
if (Number.isFinite(normalizedId) && normalizedId > 0 && !seen.has(normalizedId)) {
|
|
1471
|
+
seen.add(normalizedId);
|
|
1472
|
+
ids.push(normalizedId);
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
};
|
|
1476
|
+
append(rawValue);
|
|
1477
|
+
return ids;
|
|
1478
|
+
}
|
|
1479
|
+
function parseActionKeyboard(rawButtons) {
|
|
1480
|
+
if (rawButtons == null) {
|
|
1481
|
+
return undefined;
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
const parsed = typeof rawButtons === 'string' ? JSON.parse(rawButtons) : rawButtons;
|
|
1485
|
+
if (Array.isArray(parsed)) {
|
|
1486
|
+
const keyboard = convertButtonsToKeyboard(parsed);
|
|
1487
|
+
return keyboard.length > 0 ? keyboard : undefined;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
catch {
|
|
1491
|
+
// Ignore invalid buttons payload and send without keyboard.
|
|
1492
|
+
}
|
|
1493
|
+
return undefined;
|
|
1494
|
+
}
|
|
1495
|
+
function collectActionMediaUrls(params) {
|
|
1496
|
+
const mediaUrls = [];
|
|
1497
|
+
const seen = new Set();
|
|
1498
|
+
const append = (value) => {
|
|
1499
|
+
if (Array.isArray(value)) {
|
|
1500
|
+
value.forEach(append);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
if (typeof value !== 'string') {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const trimmed = value.trim();
|
|
1507
|
+
if (!trimmed) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (trimmed.startsWith('[')) {
|
|
1511
|
+
try {
|
|
1512
|
+
append(JSON.parse(trimmed));
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
// Fall through to scalar handling.
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (!seen.has(trimmed)) {
|
|
1520
|
+
seen.add(trimmed);
|
|
1521
|
+
mediaUrls.push(trimmed);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
append(params.mediaUrl);
|
|
1525
|
+
append(params.mediaUrls);
|
|
1526
|
+
append(params.filePath);
|
|
1527
|
+
append(params.filePaths);
|
|
1528
|
+
return mediaUrls;
|
|
1529
|
+
}
|
|
1530
|
+
function buildActionMessageText(params) {
|
|
1531
|
+
const normalizedText = typeof params.text === 'string'
|
|
1532
|
+
? markdownToBbCode(params.text.trim())
|
|
1533
|
+
: '';
|
|
1534
|
+
if (normalizedText) {
|
|
1535
|
+
return normalizedText;
|
|
1536
|
+
}
|
|
1537
|
+
if (params.attach) {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
return params.keyboard ? ' ' : null;
|
|
1541
|
+
}
|
|
771
1542
|
function parseRegisteredCommandTrigger(callbackData) {
|
|
772
1543
|
const trimmed = callbackData.trim();
|
|
773
1544
|
const isSlashCommand = trimmed.startsWith('/');
|
|
@@ -844,6 +1615,13 @@ export function extractKeyboardFromPayload(payload) {
|
|
|
844
1615
|
}
|
|
845
1616
|
return undefined;
|
|
846
1617
|
}
|
|
1618
|
+
export function extractAttachFromPayload(payload) {
|
|
1619
|
+
const cd = payload.channelData;
|
|
1620
|
+
if (!cd)
|
|
1621
|
+
return undefined;
|
|
1622
|
+
const b24Data = cd.bitrix24;
|
|
1623
|
+
return parseActionAttach(b24Data?.attach ?? b24Data?.attachments);
|
|
1624
|
+
}
|
|
847
1625
|
/**
|
|
848
1626
|
* Extract inline button JSON embedded in message text by the agent.
|
|
849
1627
|
*
|
|
@@ -874,6 +1652,13 @@ export function extractInlineButtonsFromText(text) {
|
|
|
874
1652
|
return undefined;
|
|
875
1653
|
}
|
|
876
1654
|
}
|
|
1655
|
+
function cleanupTextAfterInlineMediaExtraction(text) {
|
|
1656
|
+
return text
|
|
1657
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
1658
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1659
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
1660
|
+
.trim();
|
|
1661
|
+
}
|
|
877
1662
|
function normalizeCommandReplyPayload(params) {
|
|
878
1663
|
const { commandName, commandParams, text, language } = params;
|
|
879
1664
|
if (commandName === 'models' && commandParams.trim() === '') {
|
|
@@ -1130,12 +1915,13 @@ export async function handleWebhookRequest(req, res) {
|
|
|
1130
1915
|
// ─── Outbound adapter helpers ────────────────────────────────────────────────
|
|
1131
1916
|
function resolveOutboundSendCtx(params) {
|
|
1132
1917
|
const { config } = resolveAccount(params.cfg, params.accountId);
|
|
1133
|
-
|
|
1918
|
+
const dialogId = normalizeBitrix24DialogTarget(params.to);
|
|
1919
|
+
if (!config.webhookUrl || !gatewayState || !dialogId)
|
|
1134
1920
|
return null;
|
|
1135
1921
|
return {
|
|
1136
1922
|
webhookUrl: config.webhookUrl,
|
|
1137
1923
|
bot: gatewayState.bot,
|
|
1138
|
-
dialogId
|
|
1924
|
+
dialogId,
|
|
1139
1925
|
};
|
|
1140
1926
|
}
|
|
1141
1927
|
function collectOutboundMediaUrls(input) {
|
|
@@ -1196,13 +1982,10 @@ export const bitrix24Plugin = {
|
|
|
1196
1982
|
inlineButtons: 'all',
|
|
1197
1983
|
},
|
|
1198
1984
|
messaging: {
|
|
1199
|
-
normalizeTarget: (raw) => raw
|
|
1985
|
+
normalizeTarget: (raw) => normalizeBitrix24DialogTarget(raw),
|
|
1200
1986
|
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
|
-
},
|
|
1987
|
+
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]".',
|
|
1988
|
+
looksLikeId: (raw, _normalized) => looksLikeBitrix24DialogTarget(raw),
|
|
1206
1989
|
},
|
|
1207
1990
|
},
|
|
1208
1991
|
config: {
|
|
@@ -1286,27 +2069,47 @@ export const bitrix24Plugin = {
|
|
|
1286
2069
|
const sendCtx = resolveOutboundSendCtx(ctx);
|
|
1287
2070
|
if (!sendCtx || !gatewayState)
|
|
1288
2071
|
throw new Error('Bitrix24 gateway not started');
|
|
2072
|
+
const text = ctx.text;
|
|
1289
2073
|
const keyboard = ctx.payload?.channelData
|
|
1290
2074
|
? extractKeyboardFromPayload({ channelData: ctx.payload.channelData })
|
|
1291
2075
|
: undefined;
|
|
2076
|
+
const attach = ctx.payload?.channelData
|
|
2077
|
+
? extractAttachFromPayload({ channelData: ctx.payload.channelData })
|
|
2078
|
+
: undefined;
|
|
1292
2079
|
const mediaUrls = collectOutboundMediaUrls({
|
|
1293
2080
|
mediaUrl: ctx.mediaUrl,
|
|
1294
2081
|
payload: ctx.payload,
|
|
1295
2082
|
});
|
|
1296
2083
|
if (mediaUrls.length > 0) {
|
|
2084
|
+
const initialMessage = !keyboard && !attach ? text || undefined : undefined;
|
|
1297
2085
|
const uploadedMessageId = await uploadOutboundMedia({
|
|
1298
2086
|
mediaService: gatewayState.mediaService,
|
|
1299
2087
|
sendCtx,
|
|
1300
2088
|
mediaUrls,
|
|
2089
|
+
initialMessage,
|
|
1301
2090
|
});
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
2091
|
+
if ((text && !initialMessage) || keyboard || attach) {
|
|
2092
|
+
if (attach) {
|
|
2093
|
+
const messageId = await gatewayState.api.sendMessage(sendCtx.webhookUrl, sendCtx.bot, sendCtx.dialogId, buildActionMessageText({ text, keyboard, attach }), {
|
|
2094
|
+
...(keyboard ? { keyboard } : {}),
|
|
2095
|
+
attach,
|
|
2096
|
+
});
|
|
2097
|
+
return { messageId: String(messageId || uploadedMessageId) };
|
|
2098
|
+
}
|
|
2099
|
+
const result = await gatewayState.sendService.sendText(sendCtx, text || '', keyboard ? { keyboard } : undefined);
|
|
1304
2100
|
return { messageId: String(result.messageId ?? uploadedMessageId) };
|
|
1305
2101
|
}
|
|
1306
2102
|
return { messageId: uploadedMessageId };
|
|
1307
2103
|
}
|
|
1308
|
-
if (
|
|
1309
|
-
|
|
2104
|
+
if (text || keyboard || attach) {
|
|
2105
|
+
if (attach) {
|
|
2106
|
+
const messageId = await gatewayState.api.sendMessage(sendCtx.webhookUrl, sendCtx.bot, sendCtx.dialogId, buildActionMessageText({ text, keyboard, attach }), {
|
|
2107
|
+
...(keyboard ? { keyboard } : {}),
|
|
2108
|
+
attach,
|
|
2109
|
+
});
|
|
2110
|
+
return { messageId: String(messageId ?? '') };
|
|
2111
|
+
}
|
|
2112
|
+
const result = await gatewayState.sendService.sendText(sendCtx, text || '', keyboard ? { keyboard } : undefined);
|
|
1310
2113
|
return { messageId: String(result.messageId ?? '') };
|
|
1311
2114
|
}
|
|
1312
2115
|
return { messageId: '' };
|
|
@@ -1314,11 +2117,13 @@ export const bitrix24Plugin = {
|
|
|
1314
2117
|
},
|
|
1315
2118
|
// ─── Actions (agent-driven: reactions, etc.) ────────────────────────────
|
|
1316
2119
|
actions: {
|
|
2120
|
+
describeMessageTool: (params) => describeBitrix24MessageTool(params),
|
|
2121
|
+
extractToolSend: (params) => extractBitrix24ToolSend(params.args),
|
|
1317
2122
|
listActions: (_params) => {
|
|
1318
|
-
return [
|
|
2123
|
+
return [...BITRIX24_DISCOVERY_ACTION_NAMES];
|
|
1319
2124
|
},
|
|
1320
2125
|
supportsAction: (params) => {
|
|
1321
|
-
return
|
|
2126
|
+
return BITRIX24_ACTION_NAMES.includes(params.action);
|
|
1322
2127
|
},
|
|
1323
2128
|
handleAction: async (ctx) => {
|
|
1324
2129
|
// Helper: wrap payload as gateway-compatible tool result
|
|
@@ -1328,33 +2133,196 @@ export const bitrix24Plugin = {
|
|
|
1328
2133
|
});
|
|
1329
2134
|
// ─── Send with buttons ──────────────────────────────────────────────
|
|
1330
2135
|
if (ctx.action === 'send') {
|
|
1331
|
-
// Only intercept send when buttons are present; otherwise let gateway handle normally
|
|
1332
2136
|
const rawButtons = ctx.params.buttons;
|
|
1333
|
-
|
|
1334
|
-
return null;
|
|
2137
|
+
const rawAttach = ctx.params.attach ?? ctx.params.attachments;
|
|
1335
2138
|
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
1336
2139
|
if (!config.webhookUrl || !gatewayState)
|
|
1337
2140
|
return null;
|
|
1338
2141
|
const bot = gatewayState.bot;
|
|
1339
|
-
const to =
|
|
2142
|
+
const to = readActionTargetParam(ctx.params, ctx.to);
|
|
1340
2143
|
if (!to) {
|
|
1341
2144
|
defaultLogger.warn('handleAction send: no "to" in params or ctx, falling back to gateway');
|
|
1342
2145
|
return null;
|
|
1343
2146
|
}
|
|
1344
2147
|
const sendCtx = { webhookUrl: config.webhookUrl, bot, dialogId: to };
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2148
|
+
const messageText = readActionTextParam(ctx.params);
|
|
2149
|
+
const message = typeof messageText === 'string' ? messageText.trim() : '';
|
|
2150
|
+
const keyboard = parseActionKeyboard(rawButtons);
|
|
2151
|
+
const attach = parseActionAttach(rawAttach);
|
|
2152
|
+
const mediaUrls = collectActionMediaUrls(ctx.params);
|
|
2153
|
+
const toolContext = ctx.toolContext;
|
|
2154
|
+
const currentToolMessageId = toolContext?.currentMessageId;
|
|
2155
|
+
const currentInboundContextKey = buildAccountMessageScopeKey(ctx.accountId, currentToolMessageId);
|
|
2156
|
+
const currentInboundContext = currentInboundContextKey
|
|
2157
|
+
? inboundMessageContextById.get(currentInboundContextKey)
|
|
2158
|
+
: undefined;
|
|
2159
|
+
const forwardAckDialogId = resolvePendingNativeForwardAckDialogId(currentToolMessageId, to, ctx.accountId);
|
|
2160
|
+
const explicitForwardMessages = readExplicitForwardMessageIds(ctx.params);
|
|
2161
|
+
const referencedReplyMessageId = parseActionMessageIds(ctx.params.replyToMessageId ?? ctx.params.replyToId)[0];
|
|
2162
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
2163
|
+
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.' });
|
|
2164
|
+
}
|
|
2165
|
+
if (explicitForwardMessages.length > 0 && mediaUrls.length > 0) {
|
|
2166
|
+
return toolResult({
|
|
2167
|
+
ok: false,
|
|
2168
|
+
reason: 'unsupported_payload_combo',
|
|
2169
|
+
hint: 'Bitrix24 native forward cannot be combined with mediaUrl/mediaUrls uploads in one send action. Send the forward separately.',
|
|
2170
|
+
});
|
|
1352
2171
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
2172
|
+
if (!message && !keyboard && !attach && mediaUrls.length === 0 && explicitForwardMessages.length === 0) {
|
|
2173
|
+
return toolResult({
|
|
2174
|
+
ok: false,
|
|
2175
|
+
reason: 'missing_payload',
|
|
2176
|
+
hint: 'Provide message text, buttons, rich attach blocks or mediaUrl/mediaUrls for Bitrix24 send.',
|
|
2177
|
+
});
|
|
1355
2178
|
}
|
|
1356
|
-
const keyboard = buttons?.length ? convertButtonsToKeyboard(buttons) : undefined;
|
|
1357
2179
|
try {
|
|
2180
|
+
if (explicitForwardMessages.length > 0) {
|
|
2181
|
+
const normalizedForwardText = normalizeForwardActionText(messageText);
|
|
2182
|
+
if (attach) {
|
|
2183
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: normalizedForwardText, keyboard, attach }), {
|
|
2184
|
+
...(keyboard ? { keyboard } : {}),
|
|
2185
|
+
attach,
|
|
2186
|
+
forwardMessages: explicitForwardMessages,
|
|
2187
|
+
});
|
|
2188
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2189
|
+
return toolResult({
|
|
2190
|
+
channel: 'bitrix24',
|
|
2191
|
+
to,
|
|
2192
|
+
via: 'direct',
|
|
2193
|
+
mediaUrl: null,
|
|
2194
|
+
forwarded: true,
|
|
2195
|
+
forwardMessages: explicitForwardMessages,
|
|
2196
|
+
result: { messageId: String(messageId ?? '') },
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
const result = await gatewayState.sendService.sendText(sendCtx, normalizedForwardText || (keyboard ? ' ' : ''), {
|
|
2200
|
+
...(keyboard ? { keyboard } : {}),
|
|
2201
|
+
forwardMessages: explicitForwardMessages,
|
|
2202
|
+
});
|
|
2203
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2204
|
+
return toolResult({
|
|
2205
|
+
channel: 'bitrix24',
|
|
2206
|
+
to,
|
|
2207
|
+
via: 'direct',
|
|
2208
|
+
mediaUrl: null,
|
|
2209
|
+
forwarded: true,
|
|
2210
|
+
forwardMessages: explicitForwardMessages,
|
|
2211
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
if (referencedReplyMessageId && !attach && mediaUrls.length === 0) {
|
|
2215
|
+
let referencedMessageText = '';
|
|
2216
|
+
try {
|
|
2217
|
+
const referencedMessage = await gatewayState.api.getMessage(config.webhookUrl, bot, referencedReplyMessageId);
|
|
2218
|
+
referencedMessageText = resolveFetchedMessageBody(referencedMessage.message?.text ?? '');
|
|
2219
|
+
}
|
|
2220
|
+
catch (err) {
|
|
2221
|
+
defaultLogger.debug('Failed to hydrate referenced Bitrix24 message for send action', err);
|
|
2222
|
+
}
|
|
2223
|
+
const normalizedReferencedMessage = normalizeComparableMessageText(referencedMessageText);
|
|
2224
|
+
const normalizedMessage = normalizeComparableMessageText(message);
|
|
2225
|
+
if (normalizedReferencedMessage && normalizedReferencedMessage === normalizedMessage) {
|
|
2226
|
+
const result = await gatewayState.sendService.sendText(sendCtx, '', { forwardMessages: [referencedReplyMessageId] });
|
|
2227
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2228
|
+
return toolResult({
|
|
2229
|
+
channel: 'bitrix24',
|
|
2230
|
+
to,
|
|
2231
|
+
via: 'direct',
|
|
2232
|
+
mediaUrl: null,
|
|
2233
|
+
forwarded: true,
|
|
2234
|
+
forwardMessages: [referencedReplyMessageId],
|
|
2235
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
const result = await gatewayState.sendService.sendText(sendCtx, message || ' ', {
|
|
2239
|
+
...(keyboard ? { keyboard } : {}),
|
|
2240
|
+
replyToMessageId: referencedReplyMessageId,
|
|
2241
|
+
});
|
|
2242
|
+
return toolResult({
|
|
2243
|
+
channel: 'bitrix24',
|
|
2244
|
+
to,
|
|
2245
|
+
via: 'direct',
|
|
2246
|
+
mediaUrl: null,
|
|
2247
|
+
replied: true,
|
|
2248
|
+
replyToMessageId: referencedReplyMessageId,
|
|
2249
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
const previousInboundMessage = findPreviousRecentInboundMessage(currentToolMessageId, ctx.accountId);
|
|
2253
|
+
if (!referencedReplyMessageId
|
|
2254
|
+
&& !attach
|
|
2255
|
+
&& mediaUrls.length === 0
|
|
2256
|
+
&& !keyboard
|
|
2257
|
+
&& previousInboundMessage
|
|
2258
|
+
&& currentInboundContext
|
|
2259
|
+
&& normalizeComparableMessageText(message)
|
|
2260
|
+
&& normalizeComparableMessageText(message) === normalizeComparableMessageText(previousInboundMessage.body)
|
|
2261
|
+
&& normalizeComparableMessageText(currentInboundContext.body) !== normalizeComparableMessageText(message)) {
|
|
2262
|
+
const result = await gatewayState.sendService.sendText(sendCtx, '', { forwardMessages: [Number(previousInboundMessage.messageId)] });
|
|
2263
|
+
markPendingNativeForwardAck(forwardAckDialogId, currentToolMessageId, ctx.accountId);
|
|
2264
|
+
return toolResult({
|
|
2265
|
+
channel: 'bitrix24',
|
|
2266
|
+
to,
|
|
2267
|
+
via: 'direct',
|
|
2268
|
+
mediaUrl: null,
|
|
2269
|
+
forwarded: true,
|
|
2270
|
+
forwardMessages: [Number(previousInboundMessage.messageId)],
|
|
2271
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
if (mediaUrls.length > 0) {
|
|
2275
|
+
const initialMessage = !keyboard && !attach ? message || undefined : undefined;
|
|
2276
|
+
const uploadedMessageId = await uploadOutboundMedia({
|
|
2277
|
+
mediaService: gatewayState.mediaService,
|
|
2278
|
+
sendCtx,
|
|
2279
|
+
mediaUrls,
|
|
2280
|
+
initialMessage,
|
|
2281
|
+
});
|
|
2282
|
+
if ((message && !initialMessage) || keyboard || attach) {
|
|
2283
|
+
if (attach) {
|
|
2284
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: message, keyboard, attach }), {
|
|
2285
|
+
...(keyboard ? { keyboard } : {}),
|
|
2286
|
+
attach,
|
|
2287
|
+
});
|
|
2288
|
+
return toolResult({
|
|
2289
|
+
channel: 'bitrix24',
|
|
2290
|
+
to,
|
|
2291
|
+
via: 'direct',
|
|
2292
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
2293
|
+
result: { messageId: String(messageId ?? uploadedMessageId) },
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
const result = await gatewayState.sendService.sendText(sendCtx, message || '', keyboard ? { keyboard } : undefined);
|
|
2297
|
+
return toolResult({
|
|
2298
|
+
channel: 'bitrix24',
|
|
2299
|
+
to,
|
|
2300
|
+
via: 'direct',
|
|
2301
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
2302
|
+
result: { messageId: String(result.messageId ?? uploadedMessageId) },
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
return toolResult({
|
|
2306
|
+
channel: 'bitrix24',
|
|
2307
|
+
to,
|
|
2308
|
+
via: 'direct',
|
|
2309
|
+
mediaUrl: mediaUrls[0] ?? null,
|
|
2310
|
+
result: { messageId: uploadedMessageId },
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
if (attach) {
|
|
2314
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: message, keyboard, attach }), {
|
|
2315
|
+
...(keyboard ? { keyboard } : {}),
|
|
2316
|
+
attach,
|
|
2317
|
+
});
|
|
2318
|
+
return toolResult({
|
|
2319
|
+
channel: 'bitrix24',
|
|
2320
|
+
to,
|
|
2321
|
+
via: 'direct',
|
|
2322
|
+
mediaUrl: null,
|
|
2323
|
+
result: { messageId: String(messageId ?? '') },
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
1358
2326
|
const result = await gatewayState.sendService.sendText(sendCtx, message || ' ', keyboard ? { keyboard } : undefined);
|
|
1359
2327
|
return toolResult({
|
|
1360
2328
|
channel: 'bitrix24',
|
|
@@ -1369,6 +2337,153 @@ export const bitrix24Plugin = {
|
|
|
1369
2337
|
return toolResult({ ok: false, error: errMsg });
|
|
1370
2338
|
}
|
|
1371
2339
|
}
|
|
2340
|
+
if (ctx.action === 'reply') {
|
|
2341
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
2342
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
2343
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
2344
|
+
}
|
|
2345
|
+
const params = ctx.params;
|
|
2346
|
+
const bot = gatewayState.bot;
|
|
2347
|
+
const to = readActionTargetParam(params, ctx.to);
|
|
2348
|
+
if (!to) {
|
|
2349
|
+
return toolResult({ ok: false, reason: 'missing_target', hint: 'Bitrix24 reply requires a target dialog id in "to" or "target". Do not retry.' });
|
|
2350
|
+
}
|
|
2351
|
+
const toolContext = ctx.toolContext;
|
|
2352
|
+
const replyToMessageId = parseActionMessageIds(params.replyToMessageId
|
|
2353
|
+
?? params.replyToId
|
|
2354
|
+
?? params.messageId
|
|
2355
|
+
?? params.message_id
|
|
2356
|
+
?? toolContext?.currentMessageId)[0];
|
|
2357
|
+
if (!replyToMessageId) {
|
|
2358
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Bitrix24 reply requires a valid target messageId. Do not retry.' });
|
|
2359
|
+
}
|
|
2360
|
+
const text = readActionTextParam(params);
|
|
2361
|
+
const normalizedText = typeof text === 'string' ? text.trim() : '';
|
|
2362
|
+
const keyboard = parseActionKeyboard(params.buttons);
|
|
2363
|
+
const rawAttach = params.attach ?? params.attachments;
|
|
2364
|
+
const attach = parseActionAttach(rawAttach);
|
|
2365
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
2366
|
+
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.' });
|
|
2367
|
+
}
|
|
2368
|
+
if (!normalizedText && !keyboard && !attach) {
|
|
2369
|
+
return toolResult({ ok: false, reason: 'missing_payload', hint: 'Provide reply text, buttons or rich attach blocks for Bitrix24 reply.' });
|
|
2370
|
+
}
|
|
2371
|
+
try {
|
|
2372
|
+
if (attach) {
|
|
2373
|
+
const messageId = await gatewayState.api.sendMessage(config.webhookUrl, bot, to, buildActionMessageText({ text: normalizedText, keyboard, attach }), {
|
|
2374
|
+
...(keyboard ? { keyboard } : {}),
|
|
2375
|
+
attach,
|
|
2376
|
+
replyToMessageId,
|
|
2377
|
+
});
|
|
2378
|
+
return toolResult({
|
|
2379
|
+
ok: true,
|
|
2380
|
+
replied: true,
|
|
2381
|
+
to,
|
|
2382
|
+
replyToMessageId,
|
|
2383
|
+
messageId: String(messageId ?? ''),
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
const result = await gatewayState.sendService.sendText({ webhookUrl: config.webhookUrl, bot, dialogId: to }, normalizedText || ' ', {
|
|
2387
|
+
...(keyboard ? { keyboard } : {}),
|
|
2388
|
+
replyToMessageId,
|
|
2389
|
+
});
|
|
2390
|
+
return toolResult({
|
|
2391
|
+
ok: true,
|
|
2392
|
+
replied: true,
|
|
2393
|
+
to,
|
|
2394
|
+
replyToMessageId,
|
|
2395
|
+
messageId: String(result.messageId ?? ''),
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
catch (err) {
|
|
2399
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2400
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to send Bitrix24 reply: ${errMsg}. Do not retry.` });
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
// Note: "forward" action is not supported by OpenClaw runtime (not in
|
|
2404
|
+
// MESSAGE_ACTION_TARGET_MODE), so the runtime blocks target before it
|
|
2405
|
+
// reaches handleAction. All forwarding goes through action "send" with
|
|
2406
|
+
// forwardMessageIds parameter instead.
|
|
2407
|
+
if (ctx.action === 'edit') {
|
|
2408
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
2409
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
2410
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
2411
|
+
}
|
|
2412
|
+
const bot = gatewayState.bot;
|
|
2413
|
+
const api = gatewayState.api;
|
|
2414
|
+
const params = ctx.params;
|
|
2415
|
+
const toolContext = ctx.toolContext;
|
|
2416
|
+
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
2417
|
+
const messageId = Number(rawMessageId);
|
|
2418
|
+
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
2419
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 edits. Do not retry.' });
|
|
2420
|
+
}
|
|
2421
|
+
let keyboard;
|
|
2422
|
+
if (params.buttons === 'N') {
|
|
2423
|
+
keyboard = 'N';
|
|
2424
|
+
}
|
|
2425
|
+
else if (params.buttons != null) {
|
|
2426
|
+
try {
|
|
2427
|
+
const parsed = typeof params.buttons === 'string'
|
|
2428
|
+
? JSON.parse(params.buttons)
|
|
2429
|
+
: params.buttons;
|
|
2430
|
+
if (Array.isArray(parsed)) {
|
|
2431
|
+
keyboard = convertButtonsToKeyboard(parsed);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
catch {
|
|
2435
|
+
// Ignore invalid buttons payload and update text only.
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
const message = readActionTextParam(params);
|
|
2439
|
+
const rawAttach = params.attach ?? params.attachments;
|
|
2440
|
+
const attach = parseActionAttach(rawAttach);
|
|
2441
|
+
if (hasMeaningfulAttachInput(rawAttach) && !attach) {
|
|
2442
|
+
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.' });
|
|
2443
|
+
}
|
|
2444
|
+
if (message == null && !keyboard && !attach) {
|
|
2445
|
+
return toolResult({ ok: false, reason: 'missing_payload', hint: 'Provide message text, buttons or rich attach blocks for Bitrix24 edits.' });
|
|
2446
|
+
}
|
|
2447
|
+
try {
|
|
2448
|
+
await api.updateMessage(config.webhookUrl, bot, messageId, message, {
|
|
2449
|
+
...(keyboard ? { keyboard } : {}),
|
|
2450
|
+
...(attach ? { attach } : {}),
|
|
2451
|
+
});
|
|
2452
|
+
return toolResult({ ok: true, edited: true, messageId });
|
|
2453
|
+
}
|
|
2454
|
+
catch (err) {
|
|
2455
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2456
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to edit message: ${errMsg}. Do not retry.` });
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (ctx.action === 'delete') {
|
|
2460
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
2461
|
+
if (!config.webhookUrl || !gatewayState) {
|
|
2462
|
+
return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
|
|
2463
|
+
}
|
|
2464
|
+
const bot = gatewayState.bot;
|
|
2465
|
+
const api = gatewayState.api;
|
|
2466
|
+
const params = ctx.params;
|
|
2467
|
+
const toolContext = ctx.toolContext;
|
|
2468
|
+
const rawMessageId = params.messageId ?? params.message_id ?? toolContext?.currentMessageId;
|
|
2469
|
+
const messageId = Number(rawMessageId);
|
|
2470
|
+
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
2471
|
+
return toolResult({ ok: false, reason: 'missing_message_id', hint: 'Valid messageId is required for Bitrix24 deletes. Do not retry.' });
|
|
2472
|
+
}
|
|
2473
|
+
const complete = params.complete == null
|
|
2474
|
+
? undefined
|
|
2475
|
+
: (params.complete === true
|
|
2476
|
+
|| params.complete === 'true'
|
|
2477
|
+
|| params.complete === 'Y');
|
|
2478
|
+
try {
|
|
2479
|
+
await api.deleteMessage(config.webhookUrl, bot, messageId, complete);
|
|
2480
|
+
return toolResult({ ok: true, deleted: true, messageId });
|
|
2481
|
+
}
|
|
2482
|
+
catch (err) {
|
|
2483
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2484
|
+
return toolResult({ ok: false, reason: 'error', hint: `Failed to delete message: ${errMsg}. Do not retry.` });
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
1372
2487
|
// ─── React ────────────────────────────────────────────────────────────
|
|
1373
2488
|
if (ctx.action !== 'react')
|
|
1374
2489
|
return null;
|
|
@@ -1418,12 +2533,14 @@ export const bitrix24Plugin = {
|
|
|
1418
2533
|
}
|
|
1419
2534
|
try {
|
|
1420
2535
|
await api.addReaction(config.webhookUrl, bot, messageId, reactionCode);
|
|
2536
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
1421
2537
|
return toolResult({ ok: true, added: emoji });
|
|
1422
2538
|
}
|
|
1423
2539
|
catch (err) {
|
|
1424
2540
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1425
2541
|
const isAlreadySet = errMsg.includes('REACTION_ALREADY_SET');
|
|
1426
2542
|
if (isAlreadySet) {
|
|
2543
|
+
markPendingNativeReactionAck(toolContext?.currentMessageId, emoji, ctx.accountId);
|
|
1427
2544
|
return toolResult({ ok: true, added: emoji, warning: 'Reaction already set.' });
|
|
1428
2545
|
}
|
|
1429
2546
|
return toolResult({ ok: false, reason: 'error', emoji, hint: `Reaction failed: ${errMsg}. Do not retry.` });
|
|
@@ -1723,9 +2840,22 @@ export const bitrix24Plugin = {
|
|
|
1723
2840
|
query: bodyWithReply,
|
|
1724
2841
|
historyCache,
|
|
1725
2842
|
});
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
2843
|
+
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
2844
|
+
const nativeReplyAgentHint = buildBitrix24NativeReplyAgentHint(msgCtx.messageId);
|
|
2845
|
+
const nativeForwardAgentHint = buildBitrix24NativeForwardAgentHint({
|
|
2846
|
+
accountId: ctx.accountId,
|
|
2847
|
+
currentBody: body,
|
|
2848
|
+
currentMessageId: msgCtx.messageId,
|
|
2849
|
+
replyToMessageId: msgCtx.replyToMessageId,
|
|
2850
|
+
historyEntries: previousEntries,
|
|
2851
|
+
});
|
|
2852
|
+
const bodyForAgent = [
|
|
2853
|
+
fileDeliveryAgentHint,
|
|
2854
|
+
nativeReplyAgentHint,
|
|
2855
|
+
nativeForwardAgentHint,
|
|
2856
|
+
crossChatContext,
|
|
2857
|
+
bodyWithReply,
|
|
2858
|
+
].filter(Boolean).join('\n\n');
|
|
1729
2859
|
const combinedBody = msgCtx.isGroup
|
|
1730
2860
|
? buildHistoryContext({
|
|
1731
2861
|
entries: previousEntries,
|
|
@@ -1733,6 +2863,7 @@ export const bitrix24Plugin = {
|
|
|
1733
2863
|
})
|
|
1734
2864
|
: bodyForAgent;
|
|
1735
2865
|
recordHistory(body);
|
|
2866
|
+
rememberRecentInboundMessage(conversation.dialogId, msgCtx.messageId, body, msgCtx.timestamp ?? Date.now(), ctx.accountId);
|
|
1736
2867
|
// Resolve which agent handles this conversation
|
|
1737
2868
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
1738
2869
|
cfg,
|
|
@@ -1793,7 +2924,30 @@ export const bitrix24Plugin = {
|
|
|
1793
2924
|
});
|
|
1794
2925
|
}
|
|
1795
2926
|
if (payload.text) {
|
|
1796
|
-
|
|
2927
|
+
if (consumePendingNativeForwardAck(sendCtx.dialogId, msgCtx.messageId, ctx.accountId)) {
|
|
2928
|
+
replyDelivered = true;
|
|
2929
|
+
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 forward', {
|
|
2930
|
+
senderId: msgCtx.senderId,
|
|
2931
|
+
chatId: msgCtx.chatId,
|
|
2932
|
+
messageId: msgCtx.messageId,
|
|
2933
|
+
});
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const replyDirective = extractReplyDirective({
|
|
2937
|
+
text: payload.text,
|
|
2938
|
+
currentMessageId: msgCtx.messageId,
|
|
2939
|
+
explicitReplyToId: payload.replyToId,
|
|
2940
|
+
});
|
|
2941
|
+
let text = replyDirective.cleanText;
|
|
2942
|
+
if (consumePendingNativeReactionAck(msgCtx.messageId, text, ctx.accountId)) {
|
|
2943
|
+
replyDelivered = true;
|
|
2944
|
+
logger.debug('Suppressing trailing acknowledgement after native Bitrix24 reaction', {
|
|
2945
|
+
senderId: msgCtx.senderId,
|
|
2946
|
+
chatId: msgCtx.chatId,
|
|
2947
|
+
messageId: msgCtx.messageId,
|
|
2948
|
+
});
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
1797
2951
|
let keyboard = extractKeyboardFromPayload(payload);
|
|
1798
2952
|
// Fallback: agent may embed button JSON in text as [[{...}]]
|
|
1799
2953
|
if (!keyboard) {
|
|
@@ -1803,8 +2957,52 @@ export const bitrix24Plugin = {
|
|
|
1803
2957
|
keyboard = extracted.keyboard;
|
|
1804
2958
|
}
|
|
1805
2959
|
}
|
|
2960
|
+
const nativeForwardMessageId = findNativeForwardTarget({
|
|
2961
|
+
accountId: ctx.accountId,
|
|
2962
|
+
requestText: body,
|
|
2963
|
+
deliveredText: text,
|
|
2964
|
+
historyEntries: previousEntries,
|
|
2965
|
+
currentMessageId: msgCtx.messageId,
|
|
2966
|
+
}) ?? findImplicitForwardTargetFromRecentInbound({
|
|
2967
|
+
accountId: ctx.accountId,
|
|
2968
|
+
requestText: body,
|
|
2969
|
+
deliveredText: text,
|
|
2970
|
+
currentMessageId: msgCtx.messageId,
|
|
2971
|
+
historyEntries: previousEntries,
|
|
2972
|
+
});
|
|
2973
|
+
const nativeForwardTargets = nativeForwardMessageId
|
|
2974
|
+
? extractBitrix24DialogTargetsFromText(body)
|
|
2975
|
+
: [];
|
|
2976
|
+
const nativeReplyToMessageId = nativeForwardMessageId
|
|
2977
|
+
? undefined
|
|
2978
|
+
: replyDirective.replyToMessageId;
|
|
2979
|
+
const sendOptions = {
|
|
2980
|
+
...(keyboard ? { keyboard } : {}),
|
|
2981
|
+
...(nativeReplyToMessageId ? { replyToMessageId: nativeReplyToMessageId } : {}),
|
|
2982
|
+
...(nativeForwardMessageId ? { forwardMessages: [nativeForwardMessageId] } : {}),
|
|
2983
|
+
};
|
|
1806
2984
|
replyDelivered = true;
|
|
1807
|
-
|
|
2985
|
+
if (nativeForwardMessageId && nativeForwardTargets.length > 0) {
|
|
2986
|
+
const forwardResults = await Promise.allSettled(nativeForwardTargets.map((dialogId) => sendService.sendText({ ...sendCtx, dialogId }, '', { forwardMessages: [nativeForwardMessageId] })));
|
|
2987
|
+
const firstFailure = forwardResults.find((result) => result.status === 'rejected');
|
|
2988
|
+
const hasSuccess = forwardResults.some((result) => result.status === 'fulfilled');
|
|
2989
|
+
if (firstFailure && !hasSuccess) {
|
|
2990
|
+
throw firstFailure.reason;
|
|
2991
|
+
}
|
|
2992
|
+
if (firstFailure) {
|
|
2993
|
+
logger.warn('Failed to deliver Bitrix24 native forward to one or more explicit targets', {
|
|
2994
|
+
senderId: msgCtx.senderId,
|
|
2995
|
+
chatId: msgCtx.chatId,
|
|
2996
|
+
messageId: msgCtx.messageId,
|
|
2997
|
+
targets: nativeForwardTargets,
|
|
2998
|
+
error: firstFailure.reason instanceof Error
|
|
2999
|
+
? firstFailure.reason.message
|
|
3000
|
+
: String(firstFailure.reason),
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
await sendService.sendText(sendCtx, nativeForwardMessageId ? '' : text, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
1808
3006
|
}
|
|
1809
3007
|
},
|
|
1810
3008
|
onReplyStart: async () => {
|
|
@@ -2339,10 +3537,17 @@ export const bitrix24Plugin = {
|
|
|
2339
3537
|
accountId: ctx.accountId,
|
|
2340
3538
|
peer: conversation.peer,
|
|
2341
3539
|
});
|
|
3540
|
+
const fileDeliveryAgentHint = buildBitrix24FileDeliveryAgentHint();
|
|
3541
|
+
const nativeReplyAgentHint = buildBitrix24NativeReplyAgentHint(commandMessageId);
|
|
3542
|
+
const commandBodyForAgent = [
|
|
3543
|
+
fileDeliveryAgentHint,
|
|
3544
|
+
nativeReplyAgentHint,
|
|
3545
|
+
commandText,
|
|
3546
|
+
].filter(Boolean).join('\n\n');
|
|
2342
3547
|
const slashSessionKey = `bitrix24:slash:${senderId}:${Date.now()}`;
|
|
2343
3548
|
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
2344
3549
|
Body: commandText,
|
|
2345
|
-
BodyForAgent:
|
|
3550
|
+
BodyForAgent: commandBodyForAgent,
|
|
2346
3551
|
RawBody: commandText,
|
|
2347
3552
|
CommandBody: commandText,
|
|
2348
3553
|
CommandAuthorized: true,
|
|
@@ -2380,20 +3585,29 @@ export const bitrix24Plugin = {
|
|
|
2380
3585
|
deliver: async (payload) => {
|
|
2381
3586
|
await replyStatusHeartbeat.stopAndWait();
|
|
2382
3587
|
if (payload.text) {
|
|
3588
|
+
const replyDirective = extractReplyDirective({
|
|
3589
|
+
text: payload.text,
|
|
3590
|
+
currentMessageId: commandMessageId,
|
|
3591
|
+
explicitReplyToId: payload.replyToId,
|
|
3592
|
+
});
|
|
2383
3593
|
const keyboard = extractKeyboardFromPayload(payload)
|
|
2384
3594
|
?? defaultSessionKeyboard;
|
|
2385
3595
|
const formattedPayload = normalizeCommandReplyPayload({
|
|
2386
3596
|
commandName,
|
|
2387
3597
|
commandParams,
|
|
2388
|
-
text:
|
|
3598
|
+
text: replyDirective.cleanText,
|
|
2389
3599
|
language: cmdCtx.language,
|
|
2390
3600
|
});
|
|
3601
|
+
const sendOptions = {
|
|
3602
|
+
keyboard,
|
|
3603
|
+
convertMarkdown: formattedPayload.convertMarkdown,
|
|
3604
|
+
...(replyDirective.replyToMessageId ? { replyToMessageId: replyDirective.replyToMessageId } : {}),
|
|
3605
|
+
};
|
|
2391
3606
|
if (!commandReplyDelivered) {
|
|
2392
|
-
if (isDm) {
|
|
3607
|
+
if (isDm || replyDirective.replyToMessageId) {
|
|
2393
3608
|
commandReplyDelivered = true;
|
|
2394
3609
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
2395
|
-
|
|
2396
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
3610
|
+
...sendOptions,
|
|
2397
3611
|
});
|
|
2398
3612
|
}
|
|
2399
3613
|
else {
|
|
@@ -2406,10 +3620,7 @@ export const bitrix24Plugin = {
|
|
|
2406
3620
|
return;
|
|
2407
3621
|
}
|
|
2408
3622
|
commandReplyDelivered = true;
|
|
2409
|
-
await sendService.sendText(sendCtx, formattedPayload.text,
|
|
2410
|
-
keyboard,
|
|
2411
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
2412
|
-
});
|
|
3623
|
+
await sendService.sendText(sendCtx, formattedPayload.text, sendOptions);
|
|
2413
3624
|
}
|
|
2414
3625
|
},
|
|
2415
3626
|
onReplyStart: async () => {
|