@ihazz/bitrix24 1.1.7 → 1.1.9

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/src/channel.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { createHash } from 'node:crypto';
2
- import { basename } from 'node:path';
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 { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply } from './commands.js';
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,10 +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,
41
50
  replyGenerationFailed,
51
+ welcomeKeyboardLabels,
42
52
  watchOwnerDmNotice,
43
53
  } from './i18n.js';
44
54
  import { HistoryCache } from './history-cache.js';
@@ -79,6 +89,9 @@ const CROSS_CHAT_HISTORY_LIMIT = 20;
79
89
  const ACCESS_DENIED_REACTION = 'crossMark';
80
90
  const BOT_MESSAGE_WATCH_REACTION = 'eyes';
81
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;
82
95
  const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
83
96
 
84
97
  // ─── Emoji → B24 reaction code mapping ──────────────────────────────────
@@ -181,7 +194,7 @@ function buildTopicsBbCode(topics: string[] | undefined): string | undefined {
181
194
  }
182
195
 
183
196
  function formatQuoteTimestamp(timestamp: number | undefined, language: string | undefined): string {
184
- const locale = (language ?? 'ru').toLowerCase().slice(0, 2);
197
+ const locale = (language ?? 'en').toLowerCase().slice(0, 2);
185
198
  const value = timestamp ?? Date.now();
186
199
 
187
200
  try {
@@ -193,7 +206,7 @@ function formatQuoteTimestamp(timestamp: number | undefined, language: string |
193
206
  minute: '2-digit',
194
207
  }).format(new Date(value)).replace(',', '');
195
208
  } catch {
196
- return new Intl.DateTimeFormat('ru', {
209
+ return new Intl.DateTimeFormat('en', {
197
210
  year: 'numeric',
198
211
  month: '2-digit',
199
212
  day: '2-digit',
@@ -203,6 +216,14 @@ function formatQuoteTimestamp(timestamp: number | undefined, language: string |
203
216
  }
204
217
  }
205
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
+
206
227
  function buildWatchQuoteText(params: {
207
228
  senderName: string;
208
229
  language?: string;
@@ -505,8 +526,116 @@ export function resolveConversationRef(params: {
505
526
  export function buildConversationSessionKey(
506
527
  routeSessionKey: string,
507
528
  conversation: Pick<Bitrix24ConversationRef, 'address'>,
529
+ sessionNamespace?: string,
508
530
  ): string {
509
- return `${routeSessionKey}:${conversation.address}`;
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
+ }
510
639
  }
511
640
 
512
641
  function buildHistoryBody(msgCtx: B24MsgContext): string {
@@ -737,7 +866,7 @@ function normalizeTopicText(text: string): string {
737
866
 
738
867
  function tokenizeTopicText(text: string): string[] {
739
868
  return normalizeTopicText(text)
740
- .split(/[^a-zа-яё0-9]+/i)
869
+ .split(/[^\p{L}\p{N}]+/u)
741
870
  .filter(Boolean);
742
871
  }
743
872
 
@@ -922,7 +1051,20 @@ export function __setGatewayStateForTests(state: GatewayState | null): void {
922
1051
  gatewayState = state;
923
1052
  }
924
1053
 
925
- // ─── Default command keyboard ────────────────────────────────────────────────
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
+ }
926
1068
 
927
1069
  /** Default keyboard shown with command responses and welcome messages. */
928
1070
  export function buildDefaultCommandKeyboard(language?: string): B24Keyboard {
@@ -939,6 +1081,7 @@ export function buildDefaultCommandKeyboard(language?: string): B24Keyboard {
939
1081
  }
940
1082
 
941
1083
  export const DEFAULT_COMMAND_KEYBOARD: B24Keyboard = buildDefaultCommandKeyboard();
1084
+ export const DEFAULT_WELCOME_KEYBOARD: B24Keyboard = buildWelcomeKeyboard();
942
1085
 
943
1086
  // ─── Keyboard / Button conversion ────────────────────────────────────────────
944
1087
 
@@ -1032,7 +1175,8 @@ export function extractKeyboardFromPayload(
1032
1175
 
1033
1176
  const tgData = cd.telegram as { buttons?: ChannelButton[][] } | undefined;
1034
1177
  if (tgData?.buttons?.length) {
1035
- return convertButtonsToKeyboard(tgData.buttons);
1178
+ const keyboard = convertButtonsToKeyboard(tgData.buttons);
1179
+ return keyboard.length > 0 ? keyboard : undefined;
1036
1180
  }
1037
1181
 
1038
1182
  return undefined;
@@ -1086,6 +1230,13 @@ function normalizeCommandReplyPayload(params: {
1086
1230
  }
1087
1231
  }
1088
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
+
1089
1240
  return { text };
1090
1241
  }
1091
1242
 
@@ -1170,7 +1321,7 @@ async function sendInitialWelcomeToWebhookOwner(params: {
1170
1321
  const text = onboardingMessage(language, config.botName ?? 'OpenClaw', config.dmPolicy);
1171
1322
  const options = isPairing
1172
1323
  ? undefined
1173
- : { keyboard: buildDefaultCommandKeyboard(language) };
1324
+ : { keyboard: buildWelcomeKeyboard(language) };
1174
1325
 
1175
1326
  try {
1176
1327
  await sendService.sendText(sendCtx, text, options);
@@ -1307,8 +1458,7 @@ async function ensureCommandsRegistered(
1307
1458
  try {
1308
1459
  await api.registerCommand(webhookUrl, bot, {
1309
1460
  command: cmd.command,
1310
- title: { en: cmd.en, ru: cmd.ru },
1311
- ...(cmd.params ? { params: { en: cmd.params, ru: cmd.params } } : {}),
1461
+ ...getCommandRegistrationPayload(cmd),
1312
1462
  });
1313
1463
  registered++;
1314
1464
  } catch (err: unknown) {
@@ -1817,6 +1967,90 @@ export const bitrix24Plugin = {
1817
1967
  const welcomedDialogs = new Set<string>();
1818
1968
  const dialogNoticeTimestamps = new Map<string, number>();
1819
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();
1820
2054
 
1821
2055
  // Cleanup stale denied dialog entries once per day
1822
2056
  const DENIED_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -2092,7 +2326,7 @@ export const bitrix24Plugin = {
2092
2326
  RawBody: body,
2093
2327
  From: conversation.address,
2094
2328
  To: conversation.address,
2095
- SessionKey: buildConversationSessionKey(route.sessionKey, conversation),
2329
+ SessionKey: resolveActiveConversationSessionKey(route.sessionKey, conversation),
2096
2330
  AccountId: route.accountId,
2097
2331
  ChatType: msgCtx.isDm ? 'direct' : 'group',
2098
2332
  ConversationLabel: msgCtx.senderName,
@@ -2286,6 +2520,19 @@ export const bitrix24Plugin = {
2286
2520
  bot,
2287
2521
  dialogId: ownerId,
2288
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
+ };
2289
2536
 
2290
2537
  const noticeText = watchOwnerDmNotice(msgCtx.language, {
2291
2538
  chatRef: buildChatContextUrl(
@@ -2304,31 +2551,21 @@ export const bitrix24Plugin = {
2304
2551
  convertMarkdown: false,
2305
2552
  });
2306
2553
 
2307
- if (msgCtx.eventScope === 'user' && msgCtx.isDm) {
2308
- const quoteText = buildWatchQuoteText({
2309
- senderName: msgCtx.senderName || msgCtx.chatName || msgCtx.chatId,
2310
- language: msgCtx.language,
2311
- timestamp: msgCtx.timestamp,
2312
- anchor: `#${msgCtx.chatId}:${ownerId}/${msgCtx.messageId}`,
2313
- body: msgCtx.text.trim(),
2314
- });
2315
-
2316
- await sendService.sendText(ownerSendCtx, quoteText, {
2317
- convertMarkdown: false,
2318
- });
2319
- 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();
2320
2565
  }
2321
-
2322
- await api.sendMessage(
2323
- webhookUrl,
2324
- bot,
2325
- ownerId,
2326
- null,
2327
- { forwardMessages: [forwardedMessageId] },
2328
- );
2329
2566
  return true;
2330
2567
  } catch (err) {
2331
- logger.warn('Failed to send owner watch notification with native forward', err);
2568
+ logger.warn('Failed to send owner watch notification', err);
2332
2569
  return false;
2333
2570
  }
2334
2571
  };
@@ -2607,9 +2844,10 @@ export const bitrix24Plugin = {
2607
2844
  messageId,
2608
2845
  } = cmdCtx;
2609
2846
  const isDm = chatType === 'P';
2847
+ const replyDialogId = isDm ? senderId : dialogId;
2610
2848
  const conversation = resolveConversationRef({
2611
2849
  accountId: ctx.accountId,
2612
- dialogId,
2850
+ dialogId: replyDialogId,
2613
2851
  isDirect: isDm,
2614
2852
  });
2615
2853
 
@@ -2627,7 +2865,7 @@ export const bitrix24Plugin = {
2627
2865
  const sendCtx: SendContext = {
2628
2866
  webhookUrl,
2629
2867
  bot,
2630
- dialogId: conversation.dialogId,
2868
+ dialogId: replyDialogId,
2631
2869
  };
2632
2870
 
2633
2871
  let runtime;
@@ -2707,8 +2945,6 @@ export const bitrix24Plugin = {
2707
2945
  return;
2708
2946
  }
2709
2947
 
2710
- await sendService.sendStatus(sendCtx, 'IMBOT_AGENT_ACTION_THINKING', 8);
2711
-
2712
2948
  if (accessResult === 'deny') {
2713
2949
  await sendService.markRead(sendCtx, commandMessageId);
2714
2950
  await sendService.answerCommandText(
@@ -2745,6 +2981,9 @@ export const bitrix24Plugin = {
2745
2981
  await directTextCoalescer.flush(ctx.accountId, conversation.dialogId);
2746
2982
 
2747
2983
  const defaultCommandKeyboard = buildDefaultCommandKeyboard(cmdCtx.language);
2984
+ const defaultSessionKeyboard = commandName === 'new' && commandParams.trim() === ''
2985
+ ? buildWelcomeKeyboard(cmdCtx.language)
2986
+ : defaultCommandKeyboard;
2748
2987
 
2749
2988
  if (commandName === 'help' || commandName === 'commands') {
2750
2989
  const helpText = buildCommandsHelpText(cmdCtx.language, { concise: commandName === 'help' });
@@ -2765,6 +3004,28 @@ export const bitrix24Plugin = {
2765
3004
  return;
2766
3005
  }
2767
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
+
2768
3029
  const route = runtime.channel.routing.resolveAgentRoute({
2769
3030
  cfg,
2770
3031
  channel: 'bitrix24',
@@ -2781,7 +3042,7 @@ export const bitrix24Plugin = {
2781
3042
  CommandBody: commandText,
2782
3043
  CommandAuthorized: true,
2783
3044
  CommandSource: 'native',
2784
- CommandTargetSessionKey: buildConversationSessionKey(route.sessionKey, conversation),
3045
+ CommandTargetSessionKey: resolveActiveConversationSessionKey(route.sessionKey, conversation),
2785
3046
  From: conversation.address,
2786
3047
  To: `slash:${senderId}`,
2787
3048
  SessionKey: slashSessionKey,
@@ -2817,7 +3078,7 @@ export const bitrix24Plugin = {
2817
3078
  await replyStatusHeartbeat.stopAndWait();
2818
3079
  if (payload.text) {
2819
3080
  const keyboard = extractKeyboardFromPayload(payload)
2820
- ?? defaultCommandKeyboard;
3081
+ ?? defaultSessionKeyboard;
2821
3082
  const formattedPayload = normalizeCommandReplyPayload({
2822
3083
  commandName,
2823
3084
  commandParams,
@@ -2861,12 +3122,12 @@ export const bitrix24Plugin = {
2861
3122
  const fallbackText = replyGenerationFailed(cmdCtx.language);
2862
3123
  if (isDm) {
2863
3124
  await sendService.sendText(sendCtx, fallbackText, {
2864
- keyboard: defaultCommandKeyboard,
3125
+ keyboard: defaultSessionKeyboard,
2865
3126
  convertMarkdown: false,
2866
3127
  });
2867
3128
  } else {
2868
3129
  await sendService.answerCommandText(commandSendCtx, fallbackText, {
2869
- keyboard: defaultCommandKeyboard,
3130
+ keyboard: defaultSessionKeyboard,
2870
3131
  convertMarkdown: false,
2871
3132
  });
2872
3133
  }
@@ -2882,12 +3143,12 @@ export const bitrix24Plugin = {
2882
3143
  const fallbackText = replyGenerationFailed(cmdCtx.language);
2883
3144
  if (isDm) {
2884
3145
  await sendService.sendText(sendCtx, fallbackText, {
2885
- keyboard: defaultCommandKeyboard,
3146
+ keyboard: defaultSessionKeyboard,
2886
3147
  convertMarkdown: false,
2887
3148
  });
2888
3149
  } else {
2889
3150
  await sendService.answerCommandText(commandSendCtx, fallbackText, {
2890
- keyboard: defaultCommandKeyboard,
3151
+ keyboard: defaultSessionKeyboard,
2891
3152
  convertMarkdown: false,
2892
3153
  });
2893
3154
  }
@@ -2982,7 +3243,7 @@ export const bitrix24Plugin = {
2982
3243
  await sendService.sendText(
2983
3244
  sendCtx,
2984
3245
  text,
2985
- isPairing ? undefined : { keyboard: buildDefaultCommandKeyboard(language) },
3246
+ isPairing ? undefined : { keyboard: buildWelcomeKeyboard(language) },
2986
3247
  );
2987
3248
  welcomedDialogs.add(dialogId);
2988
3249
  logger.info('Welcome message sent', { dialogId });