@ihazz/bitrix24 0.2.4 → 0.2.5

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 CHANGED
@@ -74,9 +74,9 @@ Only `webhookUrl` is required. The gateway will not start without it.
74
74
  | Parameter | Default | Description |
75
75
  |---|---|---|
76
76
  | `webhookUrl` | — | Bitrix24 REST webhook URL (**required**) |
77
+ | `callbackUrl` | — | Full public URL for bot EVENT_HANDLER (e.g. `https://your-server.com/hooks/bitrix24`). Path is auto-extracted for route registration. (**required**) |
77
78
  | `botName` | `"OpenClaw"` | Bot display name (shown in welcome message) |
78
79
  | `botCode` | `"openclaw"` | Unique bot code for `imbot.register` |
79
- | `callbackUrl` | — | Full public URL for bot EVENT_HANDLER (e.g. `https://your-server.com/hooks/bitrix24`). Path is auto-extracted for route registration. |
80
80
  | `dmPolicy` | `"open"` | Access policy: `"open"` / `"allowlist"` / `"pairing"` |
81
81
  | `allowFrom` | — | Allowed B24 user IDs (when `dmPolicy: "allowlist"`) |
82
82
  | `showTyping` | `true` | Send typing indicator before responding |
@@ -160,10 +160,10 @@ ngrok http 3000
160
160
 
161
161
  Use the HTTPS URL from ngrok as the event handler URL in Bitrix24 bot settings.
162
162
 
163
- ## Running
163
+ ## Running (restarting)
164
164
 
165
165
  ```bash
166
- openclaw start
166
+ systemctl restart openclaw
167
167
  ```
168
168
 
169
169
  On successful start, logs will show: `Bitrix24 gateway started, webhook at /hooks/bitrix24`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihazz/bitrix24",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Bitrix24 Messenger channel for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -89,7 +89,9 @@ export async function checkAccessWithPairing(params: {
89
89
  channel: 'bitrix24',
90
90
  accountId,
91
91
  });
92
- await sendReply(reply.text);
92
+ // buildPairingReply returns a string directly, not an object
93
+ const replyText = typeof reply === 'string' ? reply : (reply as { text?: string })?.text ?? String(reply);
94
+ await sendReply(replyText);
93
95
  }
94
96
 
95
97
  logger?.debug('Pairing request handled', { senderId, code, created });
package/src/channel.ts CHANGED
@@ -294,6 +294,25 @@ export async function handleWebhookRequest(req: IncomingMessage, res: ServerResp
294
294
  }
295
295
  }
296
296
 
297
+ // ─── Outbound adapter helpers ────────────────────────────────────────────────
298
+
299
+ /**
300
+ * Build a minimal SendContext from the delivery pipeline's outbound context.
301
+ * The pipeline provides `cfg` (full OpenClaw config) and `to` (normalized dialog ID).
302
+ * We resolve the account config to get `webhookUrl`.
303
+ */
304
+ function resolveOutboundSendCtx(params: {
305
+ cfg: Record<string, unknown>;
306
+ to: string;
307
+ accountId?: string;
308
+ }): { webhookUrl?: string; dialogId: string } {
309
+ const { config } = resolveAccount(params.cfg, params.accountId);
310
+ return {
311
+ webhookUrl: config.webhookUrl,
312
+ dialogId: params.to,
313
+ };
314
+ }
315
+
297
316
  /**
298
317
  * The Bitrix24 channel plugin object.
299
318
  *
@@ -322,6 +341,11 @@ export const bitrix24Plugin = {
322
341
  },
323
342
 
324
343
  messaging: {
344
+ /**
345
+ * Normalize target ID by stripping the channel prefix.
346
+ * Called by the delivery pipeline so that `to` in outbound context is a clean numeric ID.
347
+ */
348
+ normalizeTarget: (raw: string) => raw.trim().replace(/^(bitrix24|b24|bx24):/i, ''),
325
349
  targetResolver: {
326
350
  hint: 'Use a numeric chat/dialog ID, e.g. "1" or "chat42".',
327
351
  /**
@@ -372,94 +396,87 @@ export const bitrix24Plugin = {
372
396
 
373
397
  outbound: {
374
398
  deliveryMode: 'direct' as const,
399
+ textChunkLimit: 4000,
375
400
 
376
401
  /**
377
402
  * Send a text message to B24 via the bot.
378
- * Called by OpenClaw when the agent produces a response.
403
+ * Called by the OpenClaw delivery pipeline (message tool path).
404
+ *
405
+ * Context shape: { cfg, to, accountId, text, replyToId?, threadId?, ... }
379
406
  */
380
- sendText: async (params: {
407
+ sendText: async (ctx: {
408
+ cfg: Record<string, unknown>;
409
+ to: string;
410
+ accountId?: string;
381
411
  text: string;
382
- context: B24MsgContext;
383
- account: { config: { webhookUrl?: string; showTyping?: boolean } };
412
+ [key: string]: unknown;
384
413
  }) => {
385
- const { text, context, account } = params;
386
-
387
- if (!gatewayState) {
388
- return { ok: false, error: 'Gateway not started', channel: 'bitrix24' };
389
- }
390
-
391
- const { sendService } = gatewayState;
392
-
393
- const sendCtx = {
394
- webhookUrl: account.config.webhookUrl,
395
- clientEndpoint: context.clientEndpoint,
396
- botToken: context.botToken,
397
- dialogId: context.chatId,
398
- };
399
-
400
- // Send typing indicator
401
- if (account.config.showTyping !== false) {
402
- await sendService.sendTyping(sendCtx);
403
- }
404
-
405
- // Send the response
406
- const result = await sendService.sendText(sendCtx, text);
407
-
408
- return {
409
- ok: result.ok,
410
- messageId: result.messageId,
411
- channel: 'bitrix24' as const,
412
- error: result.error,
413
- };
414
+ if (!gatewayState) throw new Error('Bitrix24 gateway not started');
415
+ const sendCtx = resolveOutboundSendCtx(ctx);
416
+ const result = await gatewayState.sendService.sendText(sendCtx, ctx.text);
417
+ return { messageId: String(result.messageId ?? '') };
414
418
  },
415
419
 
416
420
  /**
417
- * Send a payload with optional channelData (keyboards, etc.) to B24.
418
- * Called by OpenClaw when the response includes channelData.
421
+ * Send a media message to B24.
422
+ * Called by the delivery pipeline when the agent sends media.
423
+ *
424
+ * Note: full media upload requires OAuth bot token (clientEndpoint + botToken)
425
+ * which is only available in the reply path (inbound webhook events).
426
+ * In the message tool path we only have the webhook URL, so we send the caption text.
419
427
  */
420
- sendPayload: async (params: {
428
+ sendMedia: async (ctx: {
429
+ cfg: Record<string, unknown>;
430
+ to: string;
431
+ accountId?: string;
421
432
  text: string;
422
- channelData?: Record<string, unknown>;
423
- context: B24MsgContext;
424
- account: { config: { webhookUrl?: string; showTyping?: boolean } };
433
+ mediaUrl?: string;
434
+ [key: string]: unknown;
425
435
  }) => {
426
- const { text, channelData, context, account } = params;
427
-
428
- if (!gatewayState) {
429
- return { ok: false, error: 'Gateway not started', channel: 'bitrix24' };
436
+ if (!gatewayState) throw new Error('Bitrix24 gateway not started');
437
+ const sendCtx = resolveOutboundSendCtx(ctx);
438
+ // Media upload via message tool path not supported (no OAuth token).
439
+ // Send caption text only.
440
+ if (ctx.text) {
441
+ const result = await gatewayState.sendService.sendText(sendCtx, ctx.text);
442
+ return { messageId: String(result.messageId ?? '') };
430
443
  }
444
+ return { messageId: '' };
445
+ },
431
446
 
432
- const { sendService } = gatewayState;
433
-
434
- const sendCtx = {
435
- webhookUrl: account.config.webhookUrl,
436
- clientEndpoint: context.clientEndpoint,
437
- botToken: context.botToken,
438
- dialogId: context.chatId,
439
- };
440
-
441
- // Send typing indicator
442
- if (account.config.showTyping !== false) {
443
- await sendService.sendTyping(sendCtx);
444
- }
447
+ /**
448
+ * Send a rich payload with optional channelData (keyboards, etc.) to B24.
449
+ * Called by the delivery pipeline when payload includes channelData.
450
+ *
451
+ * Context shape: { cfg, to, accountId, text, mediaUrl?, payload, ... }
452
+ */
453
+ sendPayload: async (ctx: {
454
+ cfg: Record<string, unknown>;
455
+ to: string;
456
+ accountId?: string;
457
+ text: string;
458
+ mediaUrl?: string;
459
+ payload?: { channelData?: Record<string, unknown>; [key: string]: unknown };
460
+ [key: string]: unknown;
461
+ }) => {
462
+ if (!gatewayState) throw new Error('Bitrix24 gateway not started');
463
+ const sendCtx = resolveOutboundSendCtx(ctx);
445
464
 
446
465
  // Extract keyboard from channelData
447
- const keyboard = channelData
448
- ? extractKeyboardFromPayload({ channelData })
466
+ const keyboard = ctx.payload?.channelData
467
+ ? extractKeyboardFromPayload({ channelData: ctx.payload.channelData })
449
468
  : undefined;
450
469
 
451
- const result = await sendService.sendText(
452
- sendCtx,
453
- text,
454
- keyboard ? { keyboard } : undefined,
455
- );
456
-
457
- return {
458
- ok: result.ok,
459
- messageId: result.messageId,
460
- channel: 'bitrix24' as const,
461
- error: result.error,
462
- };
470
+ // Send text + keyboard
471
+ if (ctx.text) {
472
+ const result = await gatewayState.sendService.sendText(
473
+ sendCtx,
474
+ ctx.text,
475
+ keyboard ? { keyboard } : undefined,
476
+ );
477
+ return { messageId: String(result.messageId ?? '') };
478
+ }
479
+ return { messageId: '' };
463
480
  },
464
481
  },
465
482
 
@@ -415,67 +415,64 @@ describe('bitrix24Plugin', () => {
415
415
  });
416
416
  });
417
417
 
418
+ describe('messaging', () => {
419
+ it('normalizeTarget strips bitrix24: prefix', () => {
420
+ expect(bitrix24Plugin.messaging.normalizeTarget('bitrix24:1')).toBe('1');
421
+ expect(bitrix24Plugin.messaging.normalizeTarget('b24:42')).toBe('42');
422
+ expect(bitrix24Plugin.messaging.normalizeTarget('bx24:100')).toBe('100');
423
+ expect(bitrix24Plugin.messaging.normalizeTarget('123')).toBe('123');
424
+ expect(bitrix24Plugin.messaging.normalizeTarget(' bitrix24:5 ')).toBe('5');
425
+ });
426
+
427
+ it('looksLikeId recognizes numeric IDs', () => {
428
+ expect(bitrix24Plugin.messaging.targetResolver.looksLikeId('bitrix24:1', '1')).toBe(true);
429
+ expect(bitrix24Plugin.messaging.targetResolver.looksLikeId('42', '42')).toBe(true);
430
+ expect(bitrix24Plugin.messaging.targetResolver.looksLikeId('abc', 'abc')).toBe(false);
431
+ });
432
+ });
433
+
418
434
  describe('outbound', () => {
419
435
  it('has direct delivery mode', () => {
420
436
  expect(bitrix24Plugin.outbound.deliveryMode).toBe('direct');
421
437
  });
422
438
 
423
- it('sendText returns error when gateway not started', async () => {
424
- const result = await bitrix24Plugin.outbound.sendText({
425
- text: 'Hello',
426
- context: {
427
- channel: 'bitrix24',
428
- senderId: '1',
429
- senderName: 'Test',
430
- chatId: '1',
431
- chatInternalId: '100',
432
- messageId: '1',
433
- text: 'Hello',
434
- isDm: true,
435
- isGroup: false,
436
- media: [],
437
- raw: {} as any,
438
- botToken: 'token',
439
- userToken: 'utoken',
440
- clientEndpoint: 'https://test.bitrix24.com/rest/',
441
- botId: 1,
442
- memberId: 'mem1',
443
- },
444
- account: { config: { webhookUrl: 'https://test.bitrix24.com/rest/1/token/' } },
445
- });
439
+ it('has textChunkLimit of 4000', () => {
440
+ expect(bitrix24Plugin.outbound.textChunkLimit).toBe(4000);
441
+ });
446
442
 
447
- expect(result.ok).toBe(false);
448
- expect(result.error).toBe('Gateway not started');
449
- expect(result.channel).toBe('bitrix24');
443
+ it('sendText throws when gateway not started', async () => {
444
+ await expect(
445
+ bitrix24Plugin.outbound.sendText({
446
+ cfg: { channels: { bitrix24: { webhookUrl: 'https://test.bitrix24.com/rest/1/token/' } } },
447
+ to: '1',
448
+ accountId: 'default',
449
+ text: 'Hello',
450
+ }),
451
+ ).rejects.toThrow('Bitrix24 gateway not started');
450
452
  });
451
453
 
452
- it('sendPayload returns error when gateway not started', async () => {
453
- const result = await bitrix24Plugin.outbound.sendPayload({
454
- text: 'Hello',
455
- channelData: { telegram: { buttons: [[{ text: 'OK' }]] } },
456
- context: {
457
- channel: 'bitrix24',
458
- senderId: '1',
459
- senderName: 'Test',
460
- chatId: '1',
461
- chatInternalId: '100',
462
- messageId: '1',
454
+ it('sendMedia throws when gateway not started', async () => {
455
+ await expect(
456
+ bitrix24Plugin.outbound.sendMedia({
457
+ cfg: { channels: { bitrix24: { webhookUrl: 'https://test.bitrix24.com/rest/1/token/' } } },
458
+ to: '1',
459
+ accountId: 'default',
463
460
  text: 'Hello',
464
- isDm: true,
465
- isGroup: false,
466
- media: [],
467
- raw: {} as any,
468
- botToken: 'token',
469
- userToken: 'utoken',
470
- clientEndpoint: 'https://test.bitrix24.com/rest/',
471
- botId: 1,
472
- memberId: 'mem1',
473
- },
474
- account: { config: { webhookUrl: 'https://test.bitrix24.com/rest/1/token/' } },
475
- });
461
+ mediaUrl: '/tmp/test.jpg',
462
+ }),
463
+ ).rejects.toThrow('Bitrix24 gateway not started');
464
+ });
476
465
 
477
- expect(result.ok).toBe(false);
478
- expect(result.error).toBe('Gateway not started');
466
+ it('sendPayload throws when gateway not started', async () => {
467
+ await expect(
468
+ bitrix24Plugin.outbound.sendPayload({
469
+ cfg: { channels: { bitrix24: { webhookUrl: 'https://test.bitrix24.com/rest/1/token/' } } },
470
+ to: '1',
471
+ accountId: 'default',
472
+ text: 'Hello',
473
+ payload: { channelData: { telegram: { buttons: [[{ text: 'OK' }]] } } },
474
+ }),
475
+ ).rejects.toThrow('Bitrix24 gateway not started');
479
476
  });
480
477
  });
481
478