@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 +3 -3
- package/package.json +1 -1
- package/src/access-control.ts +3 -1
- package/src/channel.ts +87 -70
- package/tests/channel.test.ts +48 -51
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
|
|
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
package/src/access-control.ts
CHANGED
|
@@ -89,7 +89,9 @@ export async function checkAccessWithPairing(params: {
|
|
|
89
89
|
channel: 'bitrix24',
|
|
90
90
|
accountId,
|
|
91
91
|
});
|
|
92
|
-
|
|
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
|
|
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 (
|
|
407
|
+
sendText: async (ctx: {
|
|
408
|
+
cfg: Record<string, unknown>;
|
|
409
|
+
to: string;
|
|
410
|
+
accountId?: string;
|
|
381
411
|
text: string;
|
|
382
|
-
|
|
383
|
-
account: { config: { webhookUrl?: string; showTyping?: boolean } };
|
|
412
|
+
[key: string]: unknown;
|
|
384
413
|
}) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
418
|
-
* Called by
|
|
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
|
-
|
|
428
|
+
sendMedia: async (ctx: {
|
|
429
|
+
cfg: Record<string, unknown>;
|
|
430
|
+
to: string;
|
|
431
|
+
accountId?: string;
|
|
421
432
|
text: string;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
account: { config: { webhookUrl?: string; showTyping?: boolean } };
|
|
433
|
+
mediaUrl?: string;
|
|
434
|
+
[key: string]: unknown;
|
|
425
435
|
}) => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
package/tests/channel.test.ts
CHANGED
|
@@ -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('
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
448
|
-
expect(
|
|
449
|
-
|
|
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('
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
478
|
-
expect(
|
|
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
|
|