@ihazz/bitrix24 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/channel.d.ts +8 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +270 -38
- package/dist/src/channel.js.map +1 -1
- package/dist/src/commands.d.ts +7 -4
- package/dist/src/commands.d.ts.map +1 -1
- package/dist/src/commands.js +57 -75
- package/dist/src/commands.js.map +1 -1
- package/dist/src/i18n.d.ts +34 -0
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +459 -0
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/message-utils.js +73 -7
- package/dist/src/message-utils.js.map +1 -1
- package/package.json +1 -1
- package/src/channel.ts +361 -45
- package/src/commands.ts +75 -81
- package/src/i18n.ts +525 -0
- package/src/message-utils.ts +94 -7
package/src/channel.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
4
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
5
|
import { listAccountIds, resolveAccount, getConfig } from './config.js';
|
|
5
6
|
import { Bitrix24Api } from './api.js';
|
|
@@ -11,6 +12,7 @@ import type { DownloadedMedia } from './media-service.js';
|
|
|
11
12
|
import { InboundHandler } from './inbound-handler.js';
|
|
12
13
|
import type { FetchCommandContext, FetchJoinChatContext, FetchReactionContext } from './inbound-handler.js';
|
|
13
14
|
import { PollingService } from './polling-service.js';
|
|
15
|
+
import { resolvePollingStateDir } from './state-paths.js';
|
|
14
16
|
import {
|
|
15
17
|
normalizeAllowEntry,
|
|
16
18
|
normalizeAllowList,
|
|
@@ -27,7 +29,12 @@ import { DEFAULT_AVATAR_BASE64 } from './bot-avatar.js';
|
|
|
27
29
|
import { Bitrix24ApiError, createVerboseLogger, defaultLogger, CHANNEL_PREFIX_RE } from './utils.js';
|
|
28
30
|
import { getBitrix24Runtime } from './runtime.js';
|
|
29
31
|
import type { ChannelPairingAdapter } from './runtime.js';
|
|
30
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
OPENCLAW_COMMANDS,
|
|
34
|
+
buildCommandsHelpText,
|
|
35
|
+
formatModelsCommandReply,
|
|
36
|
+
getCommandRegistrationPayload,
|
|
37
|
+
} from './commands.js';
|
|
31
38
|
import {
|
|
32
39
|
accessApproved,
|
|
33
40
|
accessDenied,
|
|
@@ -35,9 +42,13 @@ import {
|
|
|
35
42
|
groupPairingPending,
|
|
36
43
|
mediaDownloadFailed,
|
|
37
44
|
groupChatUnsupported,
|
|
45
|
+
newSessionReplyTexts,
|
|
38
46
|
onboardingMessage,
|
|
47
|
+
normalizeNewSessionReply,
|
|
39
48
|
ownerAndAllowedUsersOnly,
|
|
40
49
|
personalBotOwnerOnly,
|
|
50
|
+
replyGenerationFailed,
|
|
51
|
+
welcomeKeyboardLabels,
|
|
41
52
|
watchOwnerDmNotice,
|
|
42
53
|
} from './i18n.js';
|
|
43
54
|
import { HistoryCache } from './history-cache.js';
|
|
@@ -78,6 +89,9 @@ const CROSS_CHAT_HISTORY_LIMIT = 20;
|
|
|
78
89
|
const ACCESS_DENIED_REACTION = 'crossMark';
|
|
79
90
|
const BOT_MESSAGE_WATCH_REACTION = 'eyes';
|
|
80
91
|
const FORWARDED_CONTEXT_RANGE = 5;
|
|
92
|
+
const ACTIVE_SESSION_NAMESPACE_TTL_MS = 180 * 24 * 60 * 60 * 1000;
|
|
93
|
+
const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
|
|
94
|
+
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;
|
|
81
95
|
const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
|
|
82
96
|
|
|
83
97
|
// ─── Emoji → B24 reaction code mapping ──────────────────────────────────
|
|
@@ -180,7 +194,7 @@ function buildTopicsBbCode(topics: string[] | undefined): string | undefined {
|
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
function formatQuoteTimestamp(timestamp: number | undefined, language: string | undefined): string {
|
|
183
|
-
const locale = (language ?? '
|
|
197
|
+
const locale = (language ?? 'en').toLowerCase().slice(0, 2);
|
|
184
198
|
const value = timestamp ?? Date.now();
|
|
185
199
|
|
|
186
200
|
try {
|
|
@@ -192,7 +206,7 @@ function formatQuoteTimestamp(timestamp: number | undefined, language: string |
|
|
|
192
206
|
minute: '2-digit',
|
|
193
207
|
}).format(new Date(value)).replace(',', '');
|
|
194
208
|
} catch {
|
|
195
|
-
return new Intl.DateTimeFormat('
|
|
209
|
+
return new Intl.DateTimeFormat('en', {
|
|
196
210
|
year: 'numeric',
|
|
197
211
|
month: '2-digit',
|
|
198
212
|
day: '2-digit',
|
|
@@ -202,6 +216,14 @@ function formatQuoteTimestamp(timestamp: number | undefined, language: string |
|
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
218
|
|
|
219
|
+
function buildWatchQuoteAnchor(msgCtx: B24MsgContext, ownerId: string): string {
|
|
220
|
+
if (msgCtx.isGroup) {
|
|
221
|
+
return `#${msgCtx.chatId}/${msgCtx.messageId}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return `#${msgCtx.chatId}:${ownerId}/${msgCtx.messageId}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
205
227
|
function buildWatchQuoteText(params: {
|
|
206
228
|
senderName: string;
|
|
207
229
|
language?: string;
|
|
@@ -504,8 +526,116 @@ export function resolveConversationRef(params: {
|
|
|
504
526
|
export function buildConversationSessionKey(
|
|
505
527
|
routeSessionKey: string,
|
|
506
528
|
conversation: Pick<Bitrix24ConversationRef, 'address'>,
|
|
529
|
+
sessionNamespace?: string,
|
|
507
530
|
): string {
|
|
508
|
-
|
|
531
|
+
const baseKey = `${routeSessionKey}:${conversation.address}`;
|
|
532
|
+
return sessionNamespace
|
|
533
|
+
? `${baseKey}:${sessionNamespace}`
|
|
534
|
+
: baseKey;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
type ActiveSessionNamespaceEntry = {
|
|
538
|
+
namespace: string;
|
|
539
|
+
updatedAt: number;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
function normalizeSessionNamespaceTimestamp(value: unknown, now: number, fallback: number): number {
|
|
543
|
+
let parsed: number | undefined;
|
|
544
|
+
|
|
545
|
+
if (typeof value === 'number') {
|
|
546
|
+
parsed = value;
|
|
547
|
+
} else if (typeof value === 'string') {
|
|
548
|
+
const asNumber = Number(value);
|
|
549
|
+
parsed = Number.isFinite(asNumber) ? asNumber : Date.parse(value);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!Number.isFinite(parsed) || parsed == null || parsed <= 0) {
|
|
553
|
+
return fallback;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return Math.min(parsed, now);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isValidActiveSessionNamespace(value: unknown): value is string {
|
|
560
|
+
return typeof value === 'string' && ACTIVE_SESSION_NAMESPACE_RE.test(value);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function normalizeActiveSessionNamespaceState(
|
|
564
|
+
state: unknown,
|
|
565
|
+
accountId: string,
|
|
566
|
+
now = Date.now(),
|
|
567
|
+
): Record<string, ActiveSessionNamespaceEntry> {
|
|
568
|
+
if (!state || typeof state !== 'object') {
|
|
569
|
+
return {};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const rawState = state as {
|
|
573
|
+
updatedAt?: unknown;
|
|
574
|
+
sessions?: Record<string, unknown>;
|
|
575
|
+
};
|
|
576
|
+
const fallbackUpdatedAt = normalizeSessionNamespaceTimestamp(rawState.updatedAt, now, now);
|
|
577
|
+
const accountKeyPrefix = `${accountId}:`;
|
|
578
|
+
const entries: Array<[string, ActiveSessionNamespaceEntry]> = [];
|
|
579
|
+
|
|
580
|
+
for (const [key, value] of Object.entries(rawState.sessions ?? {})) {
|
|
581
|
+
if (!key.startsWith(accountKeyPrefix)) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let namespace: unknown;
|
|
586
|
+
let updatedAt = fallbackUpdatedAt;
|
|
587
|
+
|
|
588
|
+
if (typeof value === 'string') {
|
|
589
|
+
namespace = value;
|
|
590
|
+
} else if (value && typeof value === 'object') {
|
|
591
|
+
const rawEntry = value as {
|
|
592
|
+
namespace?: unknown;
|
|
593
|
+
updatedAt?: unknown;
|
|
594
|
+
};
|
|
595
|
+
namespace = rawEntry.namespace;
|
|
596
|
+
updatedAt = normalizeSessionNamespaceTimestamp(rawEntry.updatedAt, now, fallbackUpdatedAt);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!isValidActiveSessionNamespace(namespace)) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (now - updatedAt > ACTIVE_SESSION_NAMESPACE_TTL_MS) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
entries.push([key, { namespace, updatedAt }]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
entries.sort((left, right) => left[1].updatedAt - right[1].updatedAt);
|
|
611
|
+
return Object.fromEntries(entries.slice(-ACTIVE_SESSION_NAMESPACE_MAX_KEYS));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function pruneActiveSessionNamespaceEntries(
|
|
615
|
+
entries: Map<string, ActiveSessionNamespaceEntry>,
|
|
616
|
+
now = Date.now(),
|
|
617
|
+
): void {
|
|
618
|
+
for (const [key, value] of entries) {
|
|
619
|
+
if (!isValidActiveSessionNamespace(value?.namespace)
|
|
620
|
+
|| !Number.isFinite(value?.updatedAt)
|
|
621
|
+
|| value.updatedAt <= 0
|
|
622
|
+
|| now - value.updatedAt > ACTIVE_SESSION_NAMESPACE_TTL_MS) {
|
|
623
|
+
entries.delete(key);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (entries.size <= ACTIVE_SESSION_NAMESPACE_MAX_KEYS) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const overflow = entries.size - ACTIVE_SESSION_NAMESPACE_MAX_KEYS;
|
|
632
|
+
const staleFirst = [...entries.entries()]
|
|
633
|
+
.sort((left, right) => left[1].updatedAt - right[1].updatedAt)
|
|
634
|
+
.slice(0, overflow);
|
|
635
|
+
|
|
636
|
+
for (const [key] of staleFirst) {
|
|
637
|
+
entries.delete(key);
|
|
638
|
+
}
|
|
509
639
|
}
|
|
510
640
|
|
|
511
641
|
function buildHistoryBody(msgCtx: B24MsgContext): string {
|
|
@@ -736,7 +866,7 @@ function normalizeTopicText(text: string): string {
|
|
|
736
866
|
|
|
737
867
|
function tokenizeTopicText(text: string): string[] {
|
|
738
868
|
return normalizeTopicText(text)
|
|
739
|
-
.split(/[
|
|
869
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
740
870
|
.filter(Boolean);
|
|
741
871
|
}
|
|
742
872
|
|
|
@@ -921,7 +1051,20 @@ export function __setGatewayStateForTests(state: GatewayState | null): void {
|
|
|
921
1051
|
gatewayState = state;
|
|
922
1052
|
}
|
|
923
1053
|
|
|
924
|
-
// ───
|
|
1054
|
+
// ─── Keyboard layouts ────────────────────────────────────────────────────────
|
|
1055
|
+
|
|
1056
|
+
export function buildWelcomeKeyboard(language?: string): B24Keyboard {
|
|
1057
|
+
const labels = welcomeKeyboardLabels(language);
|
|
1058
|
+
|
|
1059
|
+
return [
|
|
1060
|
+
{ TEXT: labels.todayTasks, ACTION: 'SEND', ACTION_VALUE: labels.todayTasks, DISPLAY: 'LINE' },
|
|
1061
|
+
{ TEXT: labels.stalledDeals, ACTION: 'SEND', ACTION_VALUE: labels.stalledDeals, DISPLAY: 'LINE' },
|
|
1062
|
+
{ TYPE: 'NEWLINE' },
|
|
1063
|
+
{ TEXT: labels.newSession, COMMAND: 'new', DISPLAY: 'LINE' },
|
|
1064
|
+
{ TEXT: labels.commands, COMMAND: 'commands', DISPLAY: 'LINE' },
|
|
1065
|
+
{ TEXT: labels.help, COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
|
|
1066
|
+
];
|
|
1067
|
+
}
|
|
925
1068
|
|
|
926
1069
|
/** Default keyboard shown with command responses and welcome messages. */
|
|
927
1070
|
export function buildDefaultCommandKeyboard(language?: string): B24Keyboard {
|
|
@@ -938,6 +1081,7 @@ export function buildDefaultCommandKeyboard(language?: string): B24Keyboard {
|
|
|
938
1081
|
}
|
|
939
1082
|
|
|
940
1083
|
export const DEFAULT_COMMAND_KEYBOARD: B24Keyboard = buildDefaultCommandKeyboard();
|
|
1084
|
+
export const DEFAULT_WELCOME_KEYBOARD: B24Keyboard = buildWelcomeKeyboard();
|
|
941
1085
|
|
|
942
1086
|
// ─── Keyboard / Button conversion ────────────────────────────────────────────
|
|
943
1087
|
|
|
@@ -1031,7 +1175,8 @@ export function extractKeyboardFromPayload(
|
|
|
1031
1175
|
|
|
1032
1176
|
const tgData = cd.telegram as { buttons?: ChannelButton[][] } | undefined;
|
|
1033
1177
|
if (tgData?.buttons?.length) {
|
|
1034
|
-
|
|
1178
|
+
const keyboard = convertButtonsToKeyboard(tgData.buttons);
|
|
1179
|
+
return keyboard.length > 0 ? keyboard : undefined;
|
|
1035
1180
|
}
|
|
1036
1181
|
|
|
1037
1182
|
return undefined;
|
|
@@ -1085,6 +1230,13 @@ function normalizeCommandReplyPayload(params: {
|
|
|
1085
1230
|
}
|
|
1086
1231
|
}
|
|
1087
1232
|
|
|
1233
|
+
if (commandName === 'new' && commandParams.trim() === '') {
|
|
1234
|
+
const normalizedText = normalizeNewSessionReply(language, text);
|
|
1235
|
+
if (normalizedText) {
|
|
1236
|
+
return { text: normalizedText, convertMarkdown: false };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1088
1240
|
return { text };
|
|
1089
1241
|
}
|
|
1090
1242
|
|
|
@@ -1169,7 +1321,7 @@ async function sendInitialWelcomeToWebhookOwner(params: {
|
|
|
1169
1321
|
const text = onboardingMessage(language, config.botName ?? 'OpenClaw', config.dmPolicy);
|
|
1170
1322
|
const options = isPairing
|
|
1171
1323
|
? undefined
|
|
1172
|
-
: { keyboard:
|
|
1324
|
+
: { keyboard: buildWelcomeKeyboard(language) };
|
|
1173
1325
|
|
|
1174
1326
|
try {
|
|
1175
1327
|
await sendService.sendText(sendCtx, text, options);
|
|
@@ -1306,8 +1458,7 @@ async function ensureCommandsRegistered(
|
|
|
1306
1458
|
try {
|
|
1307
1459
|
await api.registerCommand(webhookUrl, bot, {
|
|
1308
1460
|
command: cmd.command,
|
|
1309
|
-
|
|
1310
|
-
...(cmd.params ? { params: { en: cmd.params, ru: cmd.params } } : {}),
|
|
1461
|
+
...getCommandRegistrationPayload(cmd),
|
|
1311
1462
|
});
|
|
1312
1463
|
registered++;
|
|
1313
1464
|
} catch (err: unknown) {
|
|
@@ -1816,6 +1967,90 @@ export const bitrix24Plugin = {
|
|
|
1816
1967
|
const welcomedDialogs = new Set<string>();
|
|
1817
1968
|
const dialogNoticeTimestamps = new Map<string, number>();
|
|
1818
1969
|
const historyCache = new HistoryCache({ maxKeys: HISTORY_CACHE_MAX_KEYS });
|
|
1970
|
+
const activeSessionNamespaces = new Map<string, ActiveSessionNamespaceEntry>();
|
|
1971
|
+
const sessionNamespaceStatePath = join(resolvePollingStateDir(), `session-namespaces-${ctx.accountId}.json`);
|
|
1972
|
+
let persistActiveSessionNamespacesTail: Promise<void> = Promise.resolve();
|
|
1973
|
+
|
|
1974
|
+
const buildActiveSessionNamespaceKey = (conversation: Pick<Bitrix24ConversationRef, 'address'>): string => {
|
|
1975
|
+
return `${ctx.accountId}:${conversation.address}`;
|
|
1976
|
+
};
|
|
1977
|
+
|
|
1978
|
+
const loadActiveSessionNamespaces = async (): Promise<void> => {
|
|
1979
|
+
try {
|
|
1980
|
+
const raw = await readFile(sessionNamespaceStatePath, 'utf-8');
|
|
1981
|
+
const state = JSON.parse(raw) as {
|
|
1982
|
+
sessions?: Record<string, unknown>;
|
|
1983
|
+
};
|
|
1984
|
+
const entries = normalizeActiveSessionNamespaceState(state, ctx.accountId);
|
|
1985
|
+
const persistedSessions = state.sessions ?? {};
|
|
1986
|
+
const needsCompaction = Object.keys(entries).length !== Object.keys(persistedSessions).length
|
|
1987
|
+
|| Object.values(persistedSessions).some((value) => typeof value === 'string');
|
|
1988
|
+
|
|
1989
|
+
activeSessionNamespaces.clear();
|
|
1990
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1991
|
+
activeSessionNamespaces.set(key, value);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (needsCompaction) {
|
|
1995
|
+
void persistActiveSessionNamespaces();
|
|
1996
|
+
}
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
logger.debug('Failed to load active session namespaces, starting fresh', err);
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
const persistActiveSessionNamespaces = (): Promise<void> => {
|
|
2003
|
+
persistActiveSessionNamespacesTail = persistActiveSessionNamespacesTail
|
|
2004
|
+
.catch(() => undefined)
|
|
2005
|
+
.then(async () => {
|
|
2006
|
+
try {
|
|
2007
|
+
pruneActiveSessionNamespaceEntries(activeSessionNamespaces);
|
|
2008
|
+
await mkdir(dirname(sessionNamespaceStatePath), { recursive: true });
|
|
2009
|
+
const data = JSON.stringify({
|
|
2010
|
+
updatedAt: new Date().toISOString(),
|
|
2011
|
+
sessions: Object.fromEntries(activeSessionNamespaces),
|
|
2012
|
+
}, null, 2);
|
|
2013
|
+
const tmpPath = `${sessionNamespaceStatePath}.${randomUUID()}.tmp`;
|
|
2014
|
+
await writeFile(tmpPath, data, 'utf-8');
|
|
2015
|
+
await rename(tmpPath, sessionNamespaceStatePath);
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
logger.warn('Failed to persist active session namespaces', err);
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
return persistActiveSessionNamespacesTail;
|
|
2022
|
+
};
|
|
2023
|
+
|
|
2024
|
+
const resolveActiveConversationSessionKey = (
|
|
2025
|
+
routeSessionKey: string,
|
|
2026
|
+
conversation: Pick<Bitrix24ConversationRef, 'address'>,
|
|
2027
|
+
): string => {
|
|
2028
|
+
return buildConversationSessionKey(
|
|
2029
|
+
routeSessionKey,
|
|
2030
|
+
conversation,
|
|
2031
|
+
activeSessionNamespaces.get(buildActiveSessionNamespaceKey(conversation))?.namespace,
|
|
2032
|
+
);
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
const startNewConversationSession = async (
|
|
2036
|
+
conversation: Pick<Bitrix24ConversationRef, 'address' | 'historyKey' | 'dialogId'>,
|
|
2037
|
+
): Promise<string> => {
|
|
2038
|
+
const sessionNamespace = randomUUID();
|
|
2039
|
+
pruneActiveSessionNamespaceEntries(activeSessionNamespaces);
|
|
2040
|
+
historyCache.clear(conversation.historyKey);
|
|
2041
|
+
activeSessionNamespaces.set(buildActiveSessionNamespaceKey(conversation), {
|
|
2042
|
+
namespace: sessionNamespace,
|
|
2043
|
+
updatedAt: Date.now(),
|
|
2044
|
+
});
|
|
2045
|
+
await persistActiveSessionNamespaces();
|
|
2046
|
+
logger.info('Started new local conversation session', {
|
|
2047
|
+
dialogId: conversation.dialogId,
|
|
2048
|
+
sessionNamespace,
|
|
2049
|
+
});
|
|
2050
|
+
return sessionNamespace;
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
await loadActiveSessionNamespaces();
|
|
1819
2054
|
|
|
1820
2055
|
// Cleanup stale denied dialog entries once per day
|
|
1821
2056
|
const DENIED_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -1987,6 +2222,8 @@ export const bitrix24Plugin = {
|
|
|
1987
2222
|
sendCtx,
|
|
1988
2223
|
config,
|
|
1989
2224
|
});
|
|
2225
|
+
let replyDelivered = false;
|
|
2226
|
+
let dispatchFailed = false;
|
|
1990
2227
|
|
|
1991
2228
|
// Download media files if present
|
|
1992
2229
|
let mediaFields: Record<string, unknown> = {};
|
|
@@ -2089,7 +2326,7 @@ export const bitrix24Plugin = {
|
|
|
2089
2326
|
RawBody: body,
|
|
2090
2327
|
From: conversation.address,
|
|
2091
2328
|
To: conversation.address,
|
|
2092
|
-
SessionKey:
|
|
2329
|
+
SessionKey: resolveActiveConversationSessionKey(route.sessionKey, conversation),
|
|
2093
2330
|
AccountId: route.accountId,
|
|
2094
2331
|
ChatType: msgCtx.isDm ? 'direct' : 'group',
|
|
2095
2332
|
ConversationLabel: msgCtx.senderName,
|
|
@@ -2110,7 +2347,7 @@ export const bitrix24Plugin = {
|
|
|
2110
2347
|
});
|
|
2111
2348
|
|
|
2112
2349
|
try {
|
|
2113
|
-
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
2350
|
+
const dispatchResult = await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
2114
2351
|
ctx: inboundCtx,
|
|
2115
2352
|
cfg,
|
|
2116
2353
|
dispatcherOptions: {
|
|
@@ -2137,6 +2374,7 @@ export const bitrix24Plugin = {
|
|
|
2137
2374
|
}
|
|
2138
2375
|
}
|
|
2139
2376
|
|
|
2377
|
+
replyDelivered = true;
|
|
2140
2378
|
await sendService.sendText(sendCtx, text, keyboard ? { keyboard } : undefined);
|
|
2141
2379
|
}
|
|
2142
2380
|
},
|
|
@@ -2148,11 +2386,28 @@ export const bitrix24Plugin = {
|
|
|
2148
2386
|
},
|
|
2149
2387
|
},
|
|
2150
2388
|
});
|
|
2389
|
+
|
|
2390
|
+
if (!replyDelivered && dispatchResult?.queuedFinal === false) {
|
|
2391
|
+
await sendService.sendText(
|
|
2392
|
+
sendCtx,
|
|
2393
|
+
replyGenerationFailed(msgCtx.language),
|
|
2394
|
+
{ convertMarkdown: false },
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2151
2397
|
} catch (err) {
|
|
2398
|
+
dispatchFailed = true;
|
|
2152
2399
|
logger.error('Error dispatching message to agent', { senderId: msgCtx.senderId, chatId: msgCtx.chatId, error: err });
|
|
2153
2400
|
} finally {
|
|
2154
2401
|
replyStatusHeartbeat.stop();
|
|
2155
2402
|
}
|
|
2403
|
+
|
|
2404
|
+
if (!replyDelivered && dispatchFailed) {
|
|
2405
|
+
await sendService.sendText(
|
|
2406
|
+
sendCtx,
|
|
2407
|
+
replyGenerationFailed(msgCtx.language),
|
|
2408
|
+
{ convertMarkdown: false },
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2156
2411
|
} finally {
|
|
2157
2412
|
await mediaService.cleanupDownloadedMedia(downloadedMedia.map((mediaItem) => mediaItem.path));
|
|
2158
2413
|
}
|
|
@@ -2265,6 +2520,19 @@ export const bitrix24Plugin = {
|
|
|
2265
2520
|
bot,
|
|
2266
2521
|
dialogId: ownerId,
|
|
2267
2522
|
};
|
|
2523
|
+
const sendQuotedWatchMessage = async (): Promise<void> => {
|
|
2524
|
+
const quoteText = buildWatchQuoteText({
|
|
2525
|
+
senderName: msgCtx.senderName || msgCtx.chatName || msgCtx.chatId,
|
|
2526
|
+
language: msgCtx.language,
|
|
2527
|
+
timestamp: msgCtx.timestamp,
|
|
2528
|
+
anchor: buildWatchQuoteAnchor(msgCtx, ownerId),
|
|
2529
|
+
body: msgCtx.text.trim(),
|
|
2530
|
+
});
|
|
2531
|
+
|
|
2532
|
+
await sendService.sendText(ownerSendCtx, quoteText, {
|
|
2533
|
+
convertMarkdown: false,
|
|
2534
|
+
});
|
|
2535
|
+
};
|
|
2268
2536
|
|
|
2269
2537
|
const noticeText = watchOwnerDmNotice(msgCtx.language, {
|
|
2270
2538
|
chatRef: buildChatContextUrl(
|
|
@@ -2283,31 +2551,21 @@ export const bitrix24Plugin = {
|
|
|
2283
2551
|
convertMarkdown: false,
|
|
2284
2552
|
});
|
|
2285
2553
|
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
});
|
|
2298
|
-
return true;
|
|
2554
|
+
try {
|
|
2555
|
+
await api.sendMessage(
|
|
2556
|
+
webhookUrl,
|
|
2557
|
+
bot,
|
|
2558
|
+
ownerId,
|
|
2559
|
+
null,
|
|
2560
|
+
{ forwardMessages: [forwardedMessageId] },
|
|
2561
|
+
);
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
logger.warn('Failed to send owner watch notification with native forward, falling back to quote', err);
|
|
2564
|
+
await sendQuotedWatchMessage();
|
|
2299
2565
|
}
|
|
2300
|
-
|
|
2301
|
-
await api.sendMessage(
|
|
2302
|
-
webhookUrl,
|
|
2303
|
-
bot,
|
|
2304
|
-
ownerId,
|
|
2305
|
-
null,
|
|
2306
|
-
{ forwardMessages: [forwardedMessageId] },
|
|
2307
|
-
);
|
|
2308
2566
|
return true;
|
|
2309
2567
|
} catch (err) {
|
|
2310
|
-
logger.warn('Failed to send owner watch notification
|
|
2568
|
+
logger.warn('Failed to send owner watch notification', err);
|
|
2311
2569
|
return false;
|
|
2312
2570
|
}
|
|
2313
2571
|
};
|
|
@@ -2586,9 +2844,10 @@ export const bitrix24Plugin = {
|
|
|
2586
2844
|
messageId,
|
|
2587
2845
|
} = cmdCtx;
|
|
2588
2846
|
const isDm = chatType === 'P';
|
|
2847
|
+
const replyDialogId = isDm ? senderId : dialogId;
|
|
2589
2848
|
const conversation = resolveConversationRef({
|
|
2590
2849
|
accountId: ctx.accountId,
|
|
2591
|
-
dialogId,
|
|
2850
|
+
dialogId: replyDialogId,
|
|
2592
2851
|
isDirect: isDm,
|
|
2593
2852
|
});
|
|
2594
2853
|
|
|
@@ -2606,7 +2865,7 @@ export const bitrix24Plugin = {
|
|
|
2606
2865
|
const sendCtx: SendContext = {
|
|
2607
2866
|
webhookUrl,
|
|
2608
2867
|
bot,
|
|
2609
|
-
dialogId:
|
|
2868
|
+
dialogId: replyDialogId,
|
|
2610
2869
|
};
|
|
2611
2870
|
|
|
2612
2871
|
let runtime;
|
|
@@ -2686,8 +2945,6 @@ export const bitrix24Plugin = {
|
|
|
2686
2945
|
return;
|
|
2687
2946
|
}
|
|
2688
2947
|
|
|
2689
|
-
await sendService.sendStatus(sendCtx, 'IMBOT_AGENT_ACTION_THINKING', 8);
|
|
2690
|
-
|
|
2691
2948
|
if (accessResult === 'deny') {
|
|
2692
2949
|
await sendService.markRead(sendCtx, commandMessageId);
|
|
2693
2950
|
await sendService.answerCommandText(
|
|
@@ -2724,6 +2981,9 @@ export const bitrix24Plugin = {
|
|
|
2724
2981
|
await directTextCoalescer.flush(ctx.accountId, conversation.dialogId);
|
|
2725
2982
|
|
|
2726
2983
|
const defaultCommandKeyboard = buildDefaultCommandKeyboard(cmdCtx.language);
|
|
2984
|
+
const defaultSessionKeyboard = commandName === 'new' && commandParams.trim() === ''
|
|
2985
|
+
? buildWelcomeKeyboard(cmdCtx.language)
|
|
2986
|
+
: defaultCommandKeyboard;
|
|
2727
2987
|
|
|
2728
2988
|
if (commandName === 'help' || commandName === 'commands') {
|
|
2729
2989
|
const helpText = buildCommandsHelpText(cmdCtx.language, { concise: commandName === 'help' });
|
|
@@ -2744,6 +3004,28 @@ export const bitrix24Plugin = {
|
|
|
2744
3004
|
return;
|
|
2745
3005
|
}
|
|
2746
3006
|
|
|
3007
|
+
if (commandName === 'new' && commandParams.trim() === '') {
|
|
3008
|
+
await startNewConversationSession(conversation);
|
|
3009
|
+
const startedText = newSessionReplyTexts(cmdCtx.language).started;
|
|
3010
|
+
|
|
3011
|
+
if (isDm) {
|
|
3012
|
+
await sendService.sendText(
|
|
3013
|
+
sendCtx,
|
|
3014
|
+
startedText,
|
|
3015
|
+
{ keyboard: defaultSessionKeyboard, convertMarkdown: false },
|
|
3016
|
+
);
|
|
3017
|
+
} else {
|
|
3018
|
+
await sendService.answerCommandText(
|
|
3019
|
+
commandSendCtx,
|
|
3020
|
+
startedText,
|
|
3021
|
+
{ keyboard: defaultSessionKeyboard, convertMarkdown: false },
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
await sendService.sendStatus(sendCtx, 'IMBOT_AGENT_ACTION_THINKING', 8);
|
|
3028
|
+
|
|
2747
3029
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
2748
3030
|
cfg,
|
|
2749
3031
|
channel: 'bitrix24',
|
|
@@ -2760,7 +3042,7 @@ export const bitrix24Plugin = {
|
|
|
2760
3042
|
CommandBody: commandText,
|
|
2761
3043
|
CommandAuthorized: true,
|
|
2762
3044
|
CommandSource: 'native',
|
|
2763
|
-
CommandTargetSessionKey:
|
|
3045
|
+
CommandTargetSessionKey: resolveActiveConversationSessionKey(route.sessionKey, conversation),
|
|
2764
3046
|
From: conversation.address,
|
|
2765
3047
|
To: `slash:${senderId}`,
|
|
2766
3048
|
SessionKey: slashSessionKey,
|
|
@@ -2784,10 +3066,11 @@ export const bitrix24Plugin = {
|
|
|
2784
3066
|
config,
|
|
2785
3067
|
});
|
|
2786
3068
|
let commandReplyDelivered = false;
|
|
3069
|
+
let commandDispatchFailed = false;
|
|
2787
3070
|
|
|
2788
3071
|
try {
|
|
2789
3072
|
await replyStatusHeartbeat.start();
|
|
2790
|
-
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
3073
|
+
const dispatchResult = await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
2791
3074
|
ctx: inboundCtx,
|
|
2792
3075
|
cfg,
|
|
2793
3076
|
dispatcherOptions: {
|
|
@@ -2795,7 +3078,7 @@ export const bitrix24Plugin = {
|
|
|
2795
3078
|
await replyStatusHeartbeat.stopAndWait();
|
|
2796
3079
|
if (payload.text) {
|
|
2797
3080
|
const keyboard = extractKeyboardFromPayload(payload)
|
|
2798
|
-
??
|
|
3081
|
+
?? defaultSessionKeyboard;
|
|
2799
3082
|
const formattedPayload = normalizeCommandReplyPayload({
|
|
2800
3083
|
commandName,
|
|
2801
3084
|
commandParams,
|
|
@@ -2803,13 +3086,14 @@ export const bitrix24Plugin = {
|
|
|
2803
3086
|
language: cmdCtx.language,
|
|
2804
3087
|
});
|
|
2805
3088
|
if (!commandReplyDelivered) {
|
|
2806
|
-
commandReplyDelivered = true;
|
|
2807
3089
|
if (isDm) {
|
|
3090
|
+
commandReplyDelivered = true;
|
|
2808
3091
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
2809
3092
|
keyboard,
|
|
2810
3093
|
convertMarkdown: formattedPayload.convertMarkdown,
|
|
2811
3094
|
});
|
|
2812
3095
|
} else {
|
|
3096
|
+
commandReplyDelivered = true;
|
|
2813
3097
|
await sendService.answerCommandText(commandSendCtx, formattedPayload.text, {
|
|
2814
3098
|
keyboard,
|
|
2815
3099
|
convertMarkdown: formattedPayload.convertMarkdown,
|
|
@@ -2818,6 +3102,7 @@ export const bitrix24Plugin = {
|
|
|
2818
3102
|
return;
|
|
2819
3103
|
}
|
|
2820
3104
|
|
|
3105
|
+
commandReplyDelivered = true;
|
|
2821
3106
|
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
2822
3107
|
keyboard,
|
|
2823
3108
|
convertMarkdown: formattedPayload.convertMarkdown,
|
|
@@ -2832,11 +3117,42 @@ export const bitrix24Plugin = {
|
|
|
2832
3117
|
},
|
|
2833
3118
|
},
|
|
2834
3119
|
});
|
|
3120
|
+
|
|
3121
|
+
if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
|
|
3122
|
+
const fallbackText = replyGenerationFailed(cmdCtx.language);
|
|
3123
|
+
if (isDm) {
|
|
3124
|
+
await sendService.sendText(sendCtx, fallbackText, {
|
|
3125
|
+
keyboard: defaultSessionKeyboard,
|
|
3126
|
+
convertMarkdown: false,
|
|
3127
|
+
});
|
|
3128
|
+
} else {
|
|
3129
|
+
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
3130
|
+
keyboard: defaultSessionKeyboard,
|
|
3131
|
+
convertMarkdown: false,
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
2835
3135
|
} catch (err) {
|
|
3136
|
+
commandDispatchFailed = true;
|
|
2836
3137
|
logger.error('Error dispatching command to agent', { commandName, senderId, dialogId, error: err });
|
|
2837
3138
|
} finally {
|
|
2838
3139
|
replyStatusHeartbeat.stop();
|
|
2839
3140
|
}
|
|
3141
|
+
|
|
3142
|
+
if (!commandReplyDelivered && commandDispatchFailed) {
|
|
3143
|
+
const fallbackText = replyGenerationFailed(cmdCtx.language);
|
|
3144
|
+
if (isDm) {
|
|
3145
|
+
await sendService.sendText(sendCtx, fallbackText, {
|
|
3146
|
+
keyboard: defaultSessionKeyboard,
|
|
3147
|
+
convertMarkdown: false,
|
|
3148
|
+
});
|
|
3149
|
+
} else {
|
|
3150
|
+
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
3151
|
+
keyboard: defaultSessionKeyboard,
|
|
3152
|
+
convertMarkdown: false,
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
2840
3156
|
},
|
|
2841
3157
|
|
|
2842
3158
|
onJoinChat: async (joinCtx: FetchJoinChatContext) => {
|
|
@@ -2927,7 +3243,7 @@ export const bitrix24Plugin = {
|
|
|
2927
3243
|
await sendService.sendText(
|
|
2928
3244
|
sendCtx,
|
|
2929
3245
|
text,
|
|
2930
|
-
isPairing ? undefined : { keyboard:
|
|
3246
|
+
isPairing ? undefined : { keyboard: buildWelcomeKeyboard(language) },
|
|
2931
3247
|
);
|
|
2932
3248
|
welcomedDialogs.add(dialogId);
|
|
2933
3249
|
logger.info('Welcome message sent', { dialogId });
|