@ihazz/bitrix24 0.1.6 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihazz/bitrix24",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Bitrix24 Messenger channel for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/api.ts CHANGED
@@ -307,6 +307,18 @@ export class Bitrix24Api {
307
307
  return result.result;
308
308
  }
309
309
 
310
+ async getFileInfoViaWebhook(
311
+ webhookUrl: string,
312
+ fileId: number,
313
+ ): Promise<{ DOWNLOAD_URL: string; [key: string]: unknown }> {
314
+ const result = await this.callWebhook<{ DOWNLOAD_URL: string; [key: string]: unknown }>(
315
+ webhookUrl,
316
+ 'disk.file.get',
317
+ { id: fileId },
318
+ );
319
+ return result.result;
320
+ }
321
+
310
322
  /**
311
323
  * Get the disk folder ID for a chat (needed for file uploads).
312
324
  */
package/src/channel.ts CHANGED
@@ -38,6 +38,18 @@ interface GatewayState {
38
38
 
39
39
  let gatewayState: GatewayState | null = null;
40
40
 
41
+ // ─── Default command keyboard ────────────────────────────────────────────────
42
+
43
+ /** Default keyboard shown with command responses and welcome messages. */
44
+ export const DEFAULT_COMMAND_KEYBOARD: B24Keyboard = [
45
+ { TEXT: 'Help', COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
46
+ { TEXT: 'Status', COMMAND: 'status', DISPLAY: 'LINE' },
47
+ { TEXT: 'Commands', COMMAND: 'commands', DISPLAY: 'LINE' },
48
+ { TYPE: 'NEWLINE' },
49
+ { TEXT: 'New session', COMMAND: 'new', DISPLAY: 'LINE' },
50
+ { TEXT: 'Models', COMMAND: 'models', DISPLAY: 'LINE' },
51
+ ];
52
+
41
53
  // ─── Keyboard / Button conversion ────────────────────────────────────────────
42
54
 
43
55
  /** Generic button format used by OpenClaw channelData. */
@@ -498,6 +510,7 @@ export const bitrix24Plugin = {
498
510
  extension: m.extension,
499
511
  clientEndpoint: msgCtx.clientEndpoint,
500
512
  userToken: msgCtx.userToken,
513
+ webhookUrl: config.webhookUrl,
501
514
  }),
502
515
  ),
503
516
  )).filter(Boolean) as DownloadedMedia[];
@@ -628,25 +641,40 @@ export const bitrix24Plugin = {
628
641
 
629
642
  logger.info('Inbound command', { commandName, commandParams, senderId, dialogId });
630
643
 
631
- const runtime = getBitrix24Runtime();
632
- const cfg = runtime.config.loadConfig();
644
+ let runtime;
645
+ let cfg;
646
+ try {
647
+ runtime = getBitrix24Runtime();
648
+ cfg = runtime.config.loadConfig();
649
+ } catch (err) {
650
+ logger.error('Failed to get runtime/config for command', err);
651
+ return;
652
+ }
633
653
 
634
654
  // Pairing-aware access control (commands don't send pairing replies)
635
- const accessResult = await checkAccessWithPairing({
636
- senderId,
637
- config,
638
- runtime,
639
- accountId: ctx.accountId,
640
- pairingAdapter: bitrix24Plugin.pairing,
641
- sendReply: async () => {},
642
- logger,
643
- });
655
+ let accessResult;
656
+ try {
657
+ accessResult = await checkAccessWithPairing({
658
+ senderId,
659
+ config,
660
+ runtime,
661
+ accountId: ctx.accountId,
662
+ pairingAdapter: bitrix24Plugin.pairing,
663
+ sendReply: async () => {},
664
+ logger,
665
+ });
666
+ } catch (err) {
667
+ logger.error('Access check failed for command', err);
668
+ return;
669
+ }
644
670
 
645
671
  if (accessResult !== 'allow') {
646
672
  logger.debug(`Command blocked (${accessResult})`, { senderId });
647
673
  return;
648
674
  }
649
675
 
676
+ logger.debug('Command access allowed, resolving route', { commandText });
677
+
650
678
  const route = runtime.channel.routing.resolveAgentRoute({
651
679
  cfg,
652
680
  channel: 'bitrix24',
@@ -654,8 +682,11 @@ export const bitrix24Plugin = {
654
682
  peer: { kind: isDm ? 'direct' : 'group', id: dialogId },
655
683
  });
656
684
 
657
- // Native commands use a separate slash-command session
658
- const slashSessionKey = `bitrix24:slash:${senderId}`;
685
+ logger.debug('Command route resolved', { sessionKey: route.sessionKey });
686
+
687
+ // Each command invocation gets a unique ephemeral session
688
+ // so the gateway doesn't treat it as "already handled".
689
+ const slashSessionKey = `bitrix24:slash:${senderId}:${Date.now()}`;
659
690
 
660
691
  const inboundCtx = runtime.channel.reply.finalizeInboundContext({
661
692
  Body: commandText,
@@ -689,15 +720,23 @@ export const bitrix24Plugin = {
689
720
  dialogId,
690
721
  };
691
722
 
723
+ logger.debug('Dispatching command to agent', { commandText, slashSessionKey });
724
+
692
725
  try {
693
726
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
694
727
  ctx: inboundCtx,
695
728
  cfg,
696
729
  dispatcherOptions: {
697
730
  deliver: async (payload) => {
731
+ logger.debug('Command deliver callback', {
732
+ hasText: !!payload.text,
733
+ textLen: payload.text?.length ?? 0,
734
+ hasMedia: !!(payload.mediaUrl || payload.mediaUrls?.length),
735
+ });
698
736
  if (payload.text) {
699
- const keyboard = extractKeyboardFromPayload(payload);
700
- await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
737
+ // Use agent-provided keyboard if any, otherwise re-attach default command keyboard
738
+ const keyboard = extractKeyboardFromPayload(payload) ?? DEFAULT_COMMAND_KEYBOARD;
739
+ await sendService.sendText(sendCtx, payload.text, { keyboard });
701
740
  }
702
741
  },
703
742
  onReplyStart: async () => {
@@ -710,6 +749,7 @@ export const bitrix24Plugin = {
710
749
  },
711
750
  },
712
751
  });
752
+ logger.debug('Command dispatch completed', { commandText });
713
753
  } catch (err) {
714
754
  logger.error('Error dispatching command to agent', err);
715
755
  }
@@ -728,7 +768,8 @@ export const bitrix24Plugin = {
728
768
  botEntry.client_endpoint,
729
769
  botEntry.access_token,
730
770
  dialogId,
731
- `${config.botName ?? 'OpenClaw'} ready. Send me a message to get started.`,
771
+ `${config.botName ?? 'OpenClaw'} ready. Send me a message or pick a command below.`,
772
+ { KEYBOARD: DEFAULT_COMMAND_KEYBOARD },
732
773
  );
733
774
  } catch (err) {
734
775
  logger.error('Failed to send welcome message', err);
@@ -90,16 +90,25 @@ export class MediaService {
90
90
  extension: string;
91
91
  clientEndpoint: string;
92
92
  userToken: string;
93
+ webhookUrl?: string;
93
94
  }): Promise<DownloadedMedia | null> {
94
- const { fileId, fileName, extension, clientEndpoint, userToken } = params;
95
+ const { fileId, fileName, extension, clientEndpoint, userToken, webhookUrl } = params;
95
96
 
96
97
  try {
97
98
  // Get download URL from B24 REST API
98
- const fileInfo = await this.api.getFileInfo(
99
- clientEndpoint,
100
- userToken,
101
- Number(fileId),
102
- );
99
+ // Try webhook URL first (more reliable — event tokens often lack disk scope),
100
+ // fall back to event access token.
101
+ let fileInfo: { DOWNLOAD_URL: string; [key: string]: unknown };
102
+ if (webhookUrl) {
103
+ try {
104
+ fileInfo = await this.api.getFileInfoViaWebhook(webhookUrl, Number(fileId));
105
+ } catch {
106
+ this.logger.debug('Webhook disk.file.get failed, falling back to token', { fileId });
107
+ fileInfo = await this.api.getFileInfo(clientEndpoint, userToken, Number(fileId));
108
+ }
109
+ } else {
110
+ fileInfo = await this.api.getFileInfo(clientEndpoint, userToken, Number(fileId));
111
+ }
103
112
 
104
113
  const downloadUrl = fileInfo.DOWNLOAD_URL;
105
114
  if (!downloadUrl) {