@ihazz/bitrix24 1.1.5 → 1.1.7

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
@@ -31,12 +31,14 @@ import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply } fr
31
31
  import {
32
32
  accessApproved,
33
33
  accessDenied,
34
+ commandKeyboardLabels,
34
35
  groupPairingPending,
35
36
  mediaDownloadFailed,
36
37
  groupChatUnsupported,
37
38
  onboardingMessage,
38
39
  ownerAndAllowedUsersOnly,
39
40
  personalBotOwnerOnly,
41
+ replyGenerationFailed,
40
42
  watchOwnerDmNotice,
41
43
  } from './i18n.js';
42
44
  import { HistoryCache } from './history-cache.js';
@@ -923,14 +925,20 @@ export function __setGatewayStateForTests(state: GatewayState | null): void {
923
925
  // ─── Default command keyboard ────────────────────────────────────────────────
924
926
 
925
927
  /** Default keyboard shown with command responses and welcome messages. */
926
- export const DEFAULT_COMMAND_KEYBOARD: B24Keyboard = [
927
- { TEXT: 'Help', COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
928
- { TEXT: 'Status', COMMAND: 'status', DISPLAY: 'LINE' },
929
- { TEXT: 'Commands', COMMAND: 'commands', DISPLAY: 'LINE' },
930
- { TYPE: 'NEWLINE' },
931
- { TEXT: 'New session', COMMAND: 'new', DISPLAY: 'LINE' },
932
- { TEXT: 'Models', COMMAND: 'models', DISPLAY: 'LINE' },
933
- ];
928
+ export function buildDefaultCommandKeyboard(language?: string): B24Keyboard {
929
+ const labels = commandKeyboardLabels(language);
930
+
931
+ return [
932
+ { TEXT: labels.help, COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
933
+ { TEXT: labels.status, COMMAND: 'status', DISPLAY: 'LINE' },
934
+ { TEXT: labels.commands, COMMAND: 'commands', DISPLAY: 'LINE' },
935
+ { TYPE: 'NEWLINE' },
936
+ { TEXT: labels.newSession, COMMAND: 'new', DISPLAY: 'LINE' },
937
+ { TEXT: labels.models, COMMAND: 'models', DISPLAY: 'LINE' },
938
+ ];
939
+ }
940
+
941
+ export const DEFAULT_COMMAND_KEYBOARD: B24Keyboard = buildDefaultCommandKeyboard();
934
942
 
935
943
  // ─── Keyboard / Button conversion ────────────────────────────────────────────
936
944
 
@@ -1160,7 +1168,9 @@ async function sendInitialWelcomeToWebhookOwner(params: {
1160
1168
  };
1161
1169
  const isPairing = config.dmPolicy === 'pairing';
1162
1170
  const text = onboardingMessage(language, config.botName ?? 'OpenClaw', config.dmPolicy);
1163
- const options = isPairing ? undefined : { keyboard: DEFAULT_COMMAND_KEYBOARD };
1171
+ const options = isPairing
1172
+ ? undefined
1173
+ : { keyboard: buildDefaultCommandKeyboard(language) };
1164
1174
 
1165
1175
  try {
1166
1176
  await sendService.sendText(sendCtx, text, options);
@@ -1978,6 +1988,8 @@ export const bitrix24Plugin = {
1978
1988
  sendCtx,
1979
1989
  config,
1980
1990
  });
1991
+ let replyDelivered = false;
1992
+ let dispatchFailed = false;
1981
1993
 
1982
1994
  // Download media files if present
1983
1995
  let mediaFields: Record<string, unknown> = {};
@@ -2101,7 +2113,7 @@ export const bitrix24Plugin = {
2101
2113
  });
2102
2114
 
2103
2115
  try {
2104
- await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2116
+ const dispatchResult = await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2105
2117
  ctx: inboundCtx,
2106
2118
  cfg,
2107
2119
  dispatcherOptions: {
@@ -2128,6 +2140,7 @@ export const bitrix24Plugin = {
2128
2140
  }
2129
2141
  }
2130
2142
 
2143
+ replyDelivered = true;
2131
2144
  await sendService.sendText(sendCtx, text, keyboard ? { keyboard } : undefined);
2132
2145
  }
2133
2146
  },
@@ -2139,11 +2152,28 @@ export const bitrix24Plugin = {
2139
2152
  },
2140
2153
  },
2141
2154
  });
2155
+
2156
+ if (!replyDelivered && dispatchResult?.queuedFinal === false) {
2157
+ await sendService.sendText(
2158
+ sendCtx,
2159
+ replyGenerationFailed(msgCtx.language),
2160
+ { convertMarkdown: false },
2161
+ );
2162
+ }
2142
2163
  } catch (err) {
2164
+ dispatchFailed = true;
2143
2165
  logger.error('Error dispatching message to agent', { senderId: msgCtx.senderId, chatId: msgCtx.chatId, error: err });
2144
2166
  } finally {
2145
2167
  replyStatusHeartbeat.stop();
2146
2168
  }
2169
+
2170
+ if (!replyDelivered && dispatchFailed) {
2171
+ await sendService.sendText(
2172
+ sendCtx,
2173
+ replyGenerationFailed(msgCtx.language),
2174
+ { convertMarkdown: false },
2175
+ );
2176
+ }
2147
2177
  } finally {
2148
2178
  await mediaService.cleanupDownloadedMedia(downloadedMedia.map((mediaItem) => mediaItem.path));
2149
2179
  }
@@ -2714,12 +2744,24 @@ export const bitrix24Plugin = {
2714
2744
 
2715
2745
  await directTextCoalescer.flush(ctx.accountId, conversation.dialogId);
2716
2746
 
2747
+ const defaultCommandKeyboard = buildDefaultCommandKeyboard(cmdCtx.language);
2748
+
2717
2749
  if (commandName === 'help' || commandName === 'commands') {
2718
- await sendService.answerCommandText(
2719
- commandSendCtx,
2720
- buildCommandsHelpText(cmdCtx.language, { concise: commandName === 'help' }),
2721
- { keyboard: DEFAULT_COMMAND_KEYBOARD, convertMarkdown: false },
2722
- );
2750
+ const helpText = buildCommandsHelpText(cmdCtx.language, { concise: commandName === 'help' });
2751
+
2752
+ if (isDm) {
2753
+ await sendService.sendText(
2754
+ sendCtx,
2755
+ helpText,
2756
+ { keyboard: defaultCommandKeyboard, convertMarkdown: false },
2757
+ );
2758
+ } else {
2759
+ await sendService.answerCommandText(
2760
+ commandSendCtx,
2761
+ helpText,
2762
+ { keyboard: defaultCommandKeyboard, convertMarkdown: false },
2763
+ );
2764
+ }
2723
2765
  return;
2724
2766
  }
2725
2767
 
@@ -2763,17 +2805,19 @@ export const bitrix24Plugin = {
2763
2805
  config,
2764
2806
  });
2765
2807
  let commandReplyDelivered = false;
2808
+ let commandDispatchFailed = false;
2766
2809
 
2767
2810
  try {
2768
2811
  await replyStatusHeartbeat.start();
2769
- await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2812
+ const dispatchResult = await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2770
2813
  ctx: inboundCtx,
2771
2814
  cfg,
2772
2815
  dispatcherOptions: {
2773
2816
  deliver: async (payload) => {
2774
2817
  await replyStatusHeartbeat.stopAndWait();
2775
2818
  if (payload.text) {
2776
- const keyboard = extractKeyboardFromPayload(payload) ?? DEFAULT_COMMAND_KEYBOARD;
2819
+ const keyboard = extractKeyboardFromPayload(payload)
2820
+ ?? defaultCommandKeyboard;
2777
2821
  const formattedPayload = normalizeCommandReplyPayload({
2778
2822
  commandName,
2779
2823
  commandParams,
@@ -2781,14 +2825,23 @@ export const bitrix24Plugin = {
2781
2825
  language: cmdCtx.language,
2782
2826
  });
2783
2827
  if (!commandReplyDelivered) {
2784
- commandReplyDelivered = true;
2785
- await sendService.answerCommandText(commandSendCtx, formattedPayload.text, {
2786
- keyboard,
2787
- convertMarkdown: formattedPayload.convertMarkdown,
2788
- });
2828
+ if (isDm) {
2829
+ commandReplyDelivered = true;
2830
+ await sendService.sendText(sendCtx, formattedPayload.text, {
2831
+ keyboard,
2832
+ convertMarkdown: formattedPayload.convertMarkdown,
2833
+ });
2834
+ } else {
2835
+ commandReplyDelivered = true;
2836
+ await sendService.answerCommandText(commandSendCtx, formattedPayload.text, {
2837
+ keyboard,
2838
+ convertMarkdown: formattedPayload.convertMarkdown,
2839
+ });
2840
+ }
2789
2841
  return;
2790
2842
  }
2791
2843
 
2844
+ commandReplyDelivered = true;
2792
2845
  await sendService.sendText(sendCtx, formattedPayload.text, {
2793
2846
  keyboard,
2794
2847
  convertMarkdown: formattedPayload.convertMarkdown,
@@ -2803,11 +2856,42 @@ export const bitrix24Plugin = {
2803
2856
  },
2804
2857
  },
2805
2858
  });
2859
+
2860
+ if (!commandReplyDelivered && dispatchResult?.queuedFinal === false) {
2861
+ const fallbackText = replyGenerationFailed(cmdCtx.language);
2862
+ if (isDm) {
2863
+ await sendService.sendText(sendCtx, fallbackText, {
2864
+ keyboard: defaultCommandKeyboard,
2865
+ convertMarkdown: false,
2866
+ });
2867
+ } else {
2868
+ await sendService.answerCommandText(commandSendCtx, fallbackText, {
2869
+ keyboard: defaultCommandKeyboard,
2870
+ convertMarkdown: false,
2871
+ });
2872
+ }
2873
+ }
2806
2874
  } catch (err) {
2875
+ commandDispatchFailed = true;
2807
2876
  logger.error('Error dispatching command to agent', { commandName, senderId, dialogId, error: err });
2808
2877
  } finally {
2809
2878
  replyStatusHeartbeat.stop();
2810
2879
  }
2880
+
2881
+ if (!commandReplyDelivered && commandDispatchFailed) {
2882
+ const fallbackText = replyGenerationFailed(cmdCtx.language);
2883
+ if (isDm) {
2884
+ await sendService.sendText(sendCtx, fallbackText, {
2885
+ keyboard: defaultCommandKeyboard,
2886
+ convertMarkdown: false,
2887
+ });
2888
+ } else {
2889
+ await sendService.answerCommandText(commandSendCtx, fallbackText, {
2890
+ keyboard: defaultCommandKeyboard,
2891
+ convertMarkdown: false,
2892
+ });
2893
+ }
2894
+ }
2811
2895
  },
2812
2896
 
2813
2897
  onJoinChat: async (joinCtx: FetchJoinChatContext) => {
@@ -2898,7 +2982,7 @@ export const bitrix24Plugin = {
2898
2982
  await sendService.sendText(
2899
2983
  sendCtx,
2900
2984
  text,
2901
- isPairing ? undefined : { keyboard: DEFAULT_COMMAND_KEYBOARD },
2985
+ isPairing ? undefined : { keyboard: buildDefaultCommandKeyboard(language) },
2902
2986
  );
2903
2987
  welcomedDialogs.add(dialogId);
2904
2988
  logger.info('Welcome message sent', { dialogId });
package/src/i18n.ts CHANGED
@@ -215,3 +215,73 @@ export function onboardingMessage(
215
215
  ? pairingWelcomeMessage(lang, botName)
216
216
  : welcomeMessage(lang, botName);
217
217
  }
218
+
219
+ interface CommandKeyboardLabels {
220
+ help: string;
221
+ status: string;
222
+ commands: string;
223
+ newSession: string;
224
+ models: string;
225
+ }
226
+
227
+ const I18N_COMMAND_KEYBOARD_LABELS: Record<string, CommandKeyboardLabels> = {
228
+ en: {
229
+ help: 'Help',
230
+ status: 'Status',
231
+ commands: 'Commands',
232
+ newSession: 'New session',
233
+ models: 'Models',
234
+ },
235
+ ru: {
236
+ help: 'Справка',
237
+ status: 'Статус',
238
+ commands: 'Команды',
239
+ newSession: 'Новая сессия',
240
+ models: 'Модели',
241
+ },
242
+ de: {
243
+ help: 'Hilfe',
244
+ status: 'Status',
245
+ commands: 'Befehle',
246
+ newSession: 'Neue Sitzung',
247
+ models: 'Modelle',
248
+ },
249
+ es: {
250
+ help: 'Ayuda',
251
+ status: 'Estado',
252
+ commands: 'Comandos',
253
+ newSession: 'Nueva sesion',
254
+ models: 'Modelos',
255
+ },
256
+ fr: {
257
+ help: 'Aide',
258
+ status: 'Statut',
259
+ commands: 'Commandes',
260
+ newSession: 'Nouvelle session',
261
+ models: 'Modeles',
262
+ },
263
+ pt: {
264
+ help: 'Ajuda',
265
+ status: 'Status',
266
+ commands: 'Comandos',
267
+ newSession: 'Nova sessao',
268
+ models: 'Modelos',
269
+ },
270
+ };
271
+
272
+ export function commandKeyboardLabels(lang: string | undefined): CommandKeyboardLabels {
273
+ return resolve(I18N_COMMAND_KEYBOARD_LABELS, lang);
274
+ }
275
+
276
+ const I18N_REPLY_GENERATION_FAILED: Record<string, string> = {
277
+ en: 'I could not prepare a final reply. Please try again. If your request needs web search or external tools, check that they are configured on the server.',
278
+ ru: 'Не удалось подготовить итоговый ответ. Попробуйте повторить запрос. Если вопрос требует веб-поиска или внешних инструментов, проверьте, что они настроены на сервере.',
279
+ de: 'Ich konnte keine abschliessende Antwort vorbereiten. Bitte versuchen Sie es erneut. Wenn Ihre Anfrage Websuche oder externe Tools braucht, pruefen Sie deren Server-Konfiguration.',
280
+ es: 'No pude preparar una respuesta final. Intente de nuevo. Si su solicitud necesita busqueda web o herramientas externas, verifique que esten configuradas en el servidor.',
281
+ fr: 'Je n ai pas pu preparer de reponse finale. Reessayez. Si votre demande a besoin de recherche web ou d outils externes, verifiez leur configuration sur le serveur.',
282
+ pt: 'Nao consegui preparar uma resposta final. Tente novamente. Se sua solicitacao precisa de busca na web ou ferramentas externas, verifique se elas estao configuradas no servidor.',
283
+ };
284
+
285
+ export function replyGenerationFailed(lang: string | undefined): string {
286
+ return resolve(I18N_REPLY_GENERATION_FAILED, lang);
287
+ }
@@ -214,19 +214,26 @@ function isInlineAccentToken(value: string): boolean {
214
214
  return false;
215
215
  }
216
216
 
217
+ // In Bitrix chat UI inline [CODE] is visually heavy, so inline backticks
218
+ // should default to a compact docs accent unless the token clearly looks
219
+ // like an executable/code snippet.
220
+ if (looksLikeInlineCodeSnippet(trimmed)) {
221
+ return false;
222
+ }
223
+
217
224
  if (isWrappedInlineAccentToken(trimmed)) {
218
225
  return true;
219
226
  }
220
227
 
221
- if (looksLikeInlineCodeSnippet(trimmed)) {
222
- return false;
228
+ if (isCompactInlineAccentAssignment(trimmed)) {
229
+ return true;
223
230
  }
224
231
 
225
- return isInlineAccentCandidate(trimmed);
232
+ return isInlineAccentReference(trimmed);
226
233
  }
227
234
 
228
235
  function looksLikeInlineCodeSnippet(value: string): boolean {
229
- if (/[`"'{};]/.test(value)) {
236
+ if (/`|;\s*$/.test(value)) {
230
237
  return true;
231
238
  }
232
239
 
@@ -234,13 +241,41 @@ function looksLikeInlineCodeSnippet(value: string): boolean {
234
241
  return true;
235
242
  }
236
243
 
237
- return /\b(?:const|let|var|function|class|return|await|async|import|export|from|yield|switch|case)\b/.test(value);
244
+ if (/\b(?:const|let|var|function|class|return|await|async|import|export|from|yield|switch|case|if|else|for|while|try|catch|finally|throw|new|delete|typeof|void)\b/.test(value)) {
245
+ return true;
246
+ }
247
+
248
+ if (/\s(?:[+\-*/%<>]=?|===|!==|==|!=|<=|>=|\|\||&&|\?\?)\s/.test(value)) {
249
+ return true;
250
+ }
251
+
252
+ if (looksLikeStructuredInlineSnippet(value)) {
253
+ return true;
254
+ }
255
+
256
+ return /^(?:curl|wget|npm|pnpm|yarn|bun|node|php|python(?:3)?|ssh|git|docker(?:-compose)?|kubectl|composer)\b.*(?:\||>|<|\$|\s--?[a-z])/i.test(value);
257
+ }
258
+
259
+ function looksLikeStructuredInlineSnippet(value: string): boolean {
260
+ if (/^(?:\{\}|\[\]|\(\))$/.test(value)) {
261
+ return false;
262
+ }
263
+
264
+ if (/^\{[\s\S]+\}$/.test(value)) {
265
+ return /[:,[\]{}]/.test(value.slice(1, -1));
266
+ }
267
+
268
+ if (/^\[[\s\S]+\]$/.test(value)) {
269
+ return /[:,[\]{}]/.test(value.slice(1, -1));
270
+ }
271
+
272
+ return false;
238
273
  }
239
274
 
240
275
  function isWrappedInlineAccentToken(value: string): boolean {
241
276
  const match = value.match(/^([("'`])(.+)([)"'`])$/);
242
277
  if (!match) {
243
- return false;
278
+ return /^(?:\{\}|\[\]|\(\))$/.test(value);
244
279
  }
245
280
 
246
281
  const open = match[1];
@@ -262,7 +297,59 @@ function isWrappedInlineAccentToken(value: string): boolean {
262
297
  return false;
263
298
  }
264
299
 
265
- return isInlineAccentCandidate(inner);
300
+ return isInlineAccentReference(inner);
301
+ }
302
+
303
+ function isCompactInlineAccentAssignment(value: string): boolean {
304
+ const match = value.match(/^([\p{L}_][\p{L}\p{N}._[\]-]{0,47})\s*=\s*(.+)$/u);
305
+ if (!match) {
306
+ return false;
307
+ }
308
+
309
+ const left = match[1].trim();
310
+ const right = match[2].trim();
311
+
312
+ if (!right || /\s{2,}/.test(left)) {
313
+ return false;
314
+ }
315
+
316
+ if (/^(?:const|let|var|return|await|yield|import|export|function|class|new|delete|typeof|void)$/i.test(left)) {
317
+ return false;
318
+ }
319
+
320
+ if (looksLikeInlineCodeSnippet(left)) {
321
+ return false;
322
+ }
323
+
324
+ return isInlineAccentReference(right);
325
+ }
326
+
327
+ function isInlineAccentReference(value: string): boolean {
328
+ return isInlineAccentScalar(value) || isInlineAccentCandidate(value) || isWrappedInlineAccentToken(value);
329
+ }
330
+
331
+ function isInlineAccentScalar(value: string): boolean {
332
+ if (!value || value.length > 48) {
333
+ return false;
334
+ }
335
+
336
+ if (/^(?:true|false|null|none|yes|no|on|off|\d+(?:\.\d+)?)$/i.test(value)) {
337
+ return true;
338
+ }
339
+
340
+ if (/^#(?:[\p{L}\p{N}_-]+|[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/u.test(value)) {
341
+ return true;
342
+ }
343
+
344
+ if (/^https?:\/\/[^\s]+$/i.test(value)) {
345
+ return true;
346
+ }
347
+
348
+ if (/^0x[0-9A-Fa-f]+$/.test(value)) {
349
+ return true;
350
+ }
351
+
352
+ return false;
266
353
  }
267
354
 
268
355
  function isLowercaseNaturalLanguageWord(value: string): boolean {
@@ -109,7 +109,7 @@ export class SendService {
109
109
  ctx.messageId,
110
110
  ctx.commandDialogId ?? ctx.dialogId,
111
111
  chunks[0],
112
- chunks.length === 1 && options?.keyboard
112
+ options?.keyboard
113
113
  ? { keyboard: options.keyboard }
114
114
  : undefined,
115
115
  );
@@ -119,18 +119,13 @@ export class SendService {
119
119
  }
120
120
 
121
121
  for (let i = 1; i < chunks.length; i++) {
122
- const isLast = i === chunks.length - 1;
123
- const msgOptions = isLast && options?.keyboard
124
- ? { keyboard: options.keyboard }
125
- : undefined;
126
-
127
122
  try {
128
123
  lastMessageId = await this.api.sendMessage(
129
124
  ctx.webhookUrl,
130
125
  ctx.bot,
131
126
  ctx.dialogId,
132
127
  chunks[i],
133
- msgOptions,
128
+ undefined,
134
129
  );
135
130
  } catch (error) {
136
131
  this.logger.error('Failed to send command follow-up message', { error: serializeError(error) });