@ihazz/bitrix24 0.1.4 → 0.1.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 +12 -3
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/api.ts +27 -0
- package/src/channel.ts +288 -4
- package/src/commands.ts +60 -0
- package/src/config-schema.ts +0 -1
- package/src/message-utils.ts +21 -11
- package/src/types.ts +10 -2
- package/tests/message-utils.test.ts +13 -16
package/README.md
CHANGED
|
@@ -47,15 +47,25 @@ Add to your `openclaw.json`:
|
|
|
47
47
|
"webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
|
|
48
48
|
"botName": "OpenClaw",
|
|
49
49
|
"botCode": "openclaw",
|
|
50
|
-
"callbackPath": "/hooks/bitrix24",
|
|
51
50
|
"callbackUrl": "https://your-server.com/hooks/bitrix24",
|
|
52
51
|
"dmPolicy": "open",
|
|
52
|
+
"allowFrom": ["*"],
|
|
53
53
|
"showTyping": true
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Set allow in plugin section:
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"plugins": {
|
|
63
|
+
"allow": [
|
|
64
|
+
"bitrix24"
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
59
69
|
Only `webhookUrl` is required. The gateway will not start without it.
|
|
60
70
|
|
|
61
71
|
### Configuration Options
|
|
@@ -65,8 +75,7 @@ Only `webhookUrl` is required. The gateway will not start without it.
|
|
|
65
75
|
| `webhookUrl` | — | Bitrix24 REST webhook URL (**required**) |
|
|
66
76
|
| `botName` | `"OpenClaw"` | Bot display name (shown in welcome message) |
|
|
67
77
|
| `botCode` | `"openclaw"` | Unique bot code for `imbot.register` |
|
|
68
|
-
| `
|
|
69
|
-
| `callbackUrl` | — | Full public URL for bot registration (e.g. `https://your-server.com/hooks/bitrix24`).|
|
|
78
|
+
| `callbackUrl` | — | Full public URL for bot EVENT_HANDLER (e.g. `https://your-server.com/hooks/bitrix24`). Path is auto-extracted for route registration. |
|
|
70
79
|
| `dmPolicy` | `"open"` | Access policy: `"open"` / `"allowlist"` / `"pairing"` |
|
|
71
80
|
| `allowFrom` | — | Allowed B24 user IDs (when `dmPolicy: "allowlist"`) |
|
|
72
81
|
| `showTyping` | `true` | Send typing indicator before responding |
|
package/index.ts
CHANGED
|
@@ -28,9 +28,10 @@ export default {
|
|
|
28
28
|
|
|
29
29
|
api.registerChannel({ plugin: bitrix24Plugin });
|
|
30
30
|
|
|
31
|
-
// Register HTTP webhook route
|
|
31
|
+
// Register HTTP webhook route — derive path from callbackUrl
|
|
32
32
|
const channels = api.config?.channels as Record<string, Record<string, unknown>> | undefined;
|
|
33
|
-
const
|
|
33
|
+
const callbackUrl = channels?.bitrix24?.callbackUrl as string | undefined;
|
|
34
|
+
const callbackPath = callbackUrl ? new URL(callbackUrl).pathname : '/hooks/bitrix24';
|
|
34
35
|
|
|
35
36
|
api.registerHttpRoute({
|
|
36
37
|
path: callbackPath,
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -253,6 +253,33 @@ export class Bitrix24Api {
|
|
|
253
253
|
return result.result;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
async registerCommand(
|
|
257
|
+
webhookUrl: string,
|
|
258
|
+
params: {
|
|
259
|
+
BOT_ID: number;
|
|
260
|
+
COMMAND: string;
|
|
261
|
+
COMMON?: 'Y' | 'N';
|
|
262
|
+
HIDDEN?: 'Y' | 'N';
|
|
263
|
+
EXTRANET_SUPPORT?: 'Y' | 'N';
|
|
264
|
+
LANG: Array<{ LANGUAGE_ID: string; TITLE: string; PARAMS?: string }>;
|
|
265
|
+
EVENT_COMMAND_ADD: string;
|
|
266
|
+
},
|
|
267
|
+
): Promise<number> {
|
|
268
|
+
const result = await this.callWebhook<number>(
|
|
269
|
+
webhookUrl,
|
|
270
|
+
'imbot.command.register',
|
|
271
|
+
params,
|
|
272
|
+
);
|
|
273
|
+
return result.result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async unregisterCommand(webhookUrl: string, commandId: number): Promise<boolean> {
|
|
277
|
+
const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.command.unregister', {
|
|
278
|
+
COMMAND_ID: commandId,
|
|
279
|
+
});
|
|
280
|
+
return result.result;
|
|
281
|
+
}
|
|
282
|
+
|
|
256
283
|
async unregisterBot(webhookUrl: string, botId: number): Promise<boolean> {
|
|
257
284
|
const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.unregister', {
|
|
258
285
|
BOT_ID: botId,
|
package/src/channel.ts
CHANGED
|
@@ -4,9 +4,18 @@ import { listAccountIds, resolveAccount, getConfig } from './config.js';
|
|
|
4
4
|
import { Bitrix24Api } from './api.js';
|
|
5
5
|
import { SendService } from './send-service.js';
|
|
6
6
|
import { InboundHandler } from './inbound-handler.js';
|
|
7
|
+
import { checkAccess } from './access-control.js';
|
|
7
8
|
import { defaultLogger } from './utils.js';
|
|
8
9
|
import { getBitrix24Runtime } from './runtime.js';
|
|
9
|
-
import
|
|
10
|
+
import { OPENCLAW_COMMANDS } from './commands.js';
|
|
11
|
+
import type {
|
|
12
|
+
B24MsgContext,
|
|
13
|
+
B24JoinChatEvent,
|
|
14
|
+
B24CommandEvent,
|
|
15
|
+
Bitrix24AccountConfig,
|
|
16
|
+
B24Keyboard,
|
|
17
|
+
KeyboardButton,
|
|
18
|
+
} from './types.js';
|
|
10
19
|
|
|
11
20
|
interface Logger {
|
|
12
21
|
info: (...args: unknown[]) => void;
|
|
@@ -24,6 +33,82 @@ interface GatewayState {
|
|
|
24
33
|
|
|
25
34
|
let gatewayState: GatewayState | null = null;
|
|
26
35
|
|
|
36
|
+
// ─── Keyboard / Button conversion ────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
interface TelegramButton {
|
|
39
|
+
text: string;
|
|
40
|
+
callback_data?: string;
|
|
41
|
+
style?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert Telegram-style button rows to B24 flat KEYBOARD array.
|
|
46
|
+
* Telegram format: Array<Array<{ text, callback_data, style }>>
|
|
47
|
+
* B24 format: flat array with { TYPE: 'NEWLINE' } separators between rows.
|
|
48
|
+
*/
|
|
49
|
+
function convertButtonsToKeyboard(rows: TelegramButton[][]): B24Keyboard {
|
|
50
|
+
const keyboard: B24Keyboard = [];
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < rows.length; i++) {
|
|
53
|
+
for (const btn of rows[i]) {
|
|
54
|
+
const b24Btn: KeyboardButton = { TEXT: btn.text, DISPLAY: 'LINE' };
|
|
55
|
+
|
|
56
|
+
if (btn.callback_data?.startsWith('/')) {
|
|
57
|
+
// Slash command — use COMMAND + COMMAND_PARAMS
|
|
58
|
+
const parts = btn.callback_data.substring(1).split(' ');
|
|
59
|
+
b24Btn.COMMAND = parts[0];
|
|
60
|
+
if (parts.length > 1) {
|
|
61
|
+
b24Btn.COMMAND_PARAMS = parts.slice(1).join(' ');
|
|
62
|
+
}
|
|
63
|
+
} else if (btn.callback_data) {
|
|
64
|
+
// Non-slash data — insert text into input via PUT action
|
|
65
|
+
b24Btn.ACTION = 'PUT';
|
|
66
|
+
b24Btn.ACTION_VALUE = btn.callback_data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (btn.style === 'primary') {
|
|
70
|
+
b24Btn.BG_COLOR_TOKEN = 'primary';
|
|
71
|
+
} else if (btn.style === 'attention' || btn.style === 'danger') {
|
|
72
|
+
b24Btn.BG_COLOR_TOKEN = 'alert';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
keyboard.push(b24Btn);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add NEWLINE separator between rows (not after last row)
|
|
79
|
+
if (i < rows.length - 1) {
|
|
80
|
+
keyboard.push({ TYPE: 'NEWLINE' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return keyboard;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract B24 keyboard from a dispatcher payload's channelData.
|
|
89
|
+
* Checks bitrix24-specific data first, then falls back to Telegram button format.
|
|
90
|
+
*/
|
|
91
|
+
function extractKeyboardFromPayload(
|
|
92
|
+
payload: { channelData?: Record<string, unknown> },
|
|
93
|
+
): B24Keyboard | undefined {
|
|
94
|
+
const cd = payload.channelData;
|
|
95
|
+
if (!cd) return undefined;
|
|
96
|
+
|
|
97
|
+
// Direct B24 keyboard (future-proof: channelData.bitrix24.keyboard)
|
|
98
|
+
const b24Data = cd.bitrix24 as { keyboard?: B24Keyboard } | undefined;
|
|
99
|
+
if (b24Data?.keyboard?.length) {
|
|
100
|
+
return b24Data.keyboard;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Translate from Telegram button format
|
|
104
|
+
const tgData = cd.telegram as { buttons?: TelegramButton[][] } | undefined;
|
|
105
|
+
if (tgData?.buttons?.length) {
|
|
106
|
+
return convertButtonsToKeyboard(tgData.buttons);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
27
112
|
/**
|
|
28
113
|
* Register or update the bot on the Bitrix24 portal.
|
|
29
114
|
* Checks imbot.bot.list first; if a bot with the same CODE exists, updates it.
|
|
@@ -82,6 +167,51 @@ async function ensureBotRegistered(
|
|
|
82
167
|
}
|
|
83
168
|
}
|
|
84
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Register OpenClaw slash commands with the B24 bot.
|
|
172
|
+
* Runs in the background — errors are logged but don't block startup.
|
|
173
|
+
*/
|
|
174
|
+
async function ensureCommandsRegistered(
|
|
175
|
+
api: Bitrix24Api,
|
|
176
|
+
config: Bitrix24AccountConfig,
|
|
177
|
+
botId: number,
|
|
178
|
+
logger: Logger,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const { webhookUrl, callbackUrl } = config;
|
|
181
|
+
if (!webhookUrl || !callbackUrl) return;
|
|
182
|
+
|
|
183
|
+
let registered = 0;
|
|
184
|
+
let skipped = 0;
|
|
185
|
+
|
|
186
|
+
for (const cmd of OPENCLAW_COMMANDS) {
|
|
187
|
+
try {
|
|
188
|
+
await api.registerCommand(webhookUrl, {
|
|
189
|
+
BOT_ID: botId,
|
|
190
|
+
COMMAND: cmd.command,
|
|
191
|
+
COMMON: 'N',
|
|
192
|
+
HIDDEN: 'N',
|
|
193
|
+
EXTRANET_SUPPORT: 'N',
|
|
194
|
+
LANG: [
|
|
195
|
+
{ LANGUAGE_ID: 'en', TITLE: cmd.en, ...(cmd.params ? { PARAMS: cmd.params } : {}) },
|
|
196
|
+
{ LANGUAGE_ID: 'ru', TITLE: cmd.ru, ...(cmd.params ? { PARAMS: cmd.params } : {}) },
|
|
197
|
+
],
|
|
198
|
+
EVENT_COMMAND_ADD: callbackUrl,
|
|
199
|
+
});
|
|
200
|
+
registered++;
|
|
201
|
+
} catch (err: unknown) {
|
|
202
|
+
// "WRONG_REQUEST" typically means command already exists
|
|
203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
204
|
+
if (msg.includes('WRONG_REQUEST') || msg.includes('already')) {
|
|
205
|
+
skipped++;
|
|
206
|
+
} else {
|
|
207
|
+
logger.warn(`Failed to register command /${cmd.command}`, err);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
logger.info(`Commands sync: ${registered} registered, ${skipped} already existed (total ${OPENCLAW_COMMANDS.length})`);
|
|
213
|
+
}
|
|
214
|
+
|
|
85
215
|
/**
|
|
86
216
|
* Handle an incoming HTTP request on the webhook route.
|
|
87
217
|
* Called by the HTTP route registered in index.ts.
|
|
@@ -201,6 +331,55 @@ export const bitrix24Plugin = {
|
|
|
201
331
|
error: result.error,
|
|
202
332
|
};
|
|
203
333
|
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Send a payload with optional channelData (keyboards, etc.) to B24.
|
|
337
|
+
* Called by OpenClaw when the response includes channelData.
|
|
338
|
+
*/
|
|
339
|
+
sendPayload: async (params: {
|
|
340
|
+
text: string;
|
|
341
|
+
channelData?: Record<string, unknown>;
|
|
342
|
+
context: B24MsgContext;
|
|
343
|
+
account: { config: { webhookUrl?: string; showTyping?: boolean } };
|
|
344
|
+
}) => {
|
|
345
|
+
const { text, channelData, context, account } = params;
|
|
346
|
+
|
|
347
|
+
if (!gatewayState) {
|
|
348
|
+
return { ok: false, error: 'Gateway not started', channel: 'bitrix24' };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const { sendService } = gatewayState;
|
|
352
|
+
|
|
353
|
+
const sendCtx = {
|
|
354
|
+
webhookUrl: account.config.webhookUrl,
|
|
355
|
+
clientEndpoint: context.clientEndpoint,
|
|
356
|
+
botToken: context.botToken,
|
|
357
|
+
dialogId: context.chatId,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Send typing indicator
|
|
361
|
+
if (account.config.showTyping !== false) {
|
|
362
|
+
await sendService.sendTyping(sendCtx);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Extract keyboard from channelData
|
|
366
|
+
const keyboard = channelData
|
|
367
|
+
? extractKeyboardFromPayload({ channelData })
|
|
368
|
+
: undefined;
|
|
369
|
+
|
|
370
|
+
const result = await sendService.sendText(
|
|
371
|
+
sendCtx,
|
|
372
|
+
text,
|
|
373
|
+
keyboard ? { keyboard } : undefined,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
ok: result.ok,
|
|
378
|
+
messageId: result.messageId,
|
|
379
|
+
channel: 'bitrix24' as const,
|
|
380
|
+
error: result.error,
|
|
381
|
+
};
|
|
382
|
+
},
|
|
204
383
|
},
|
|
205
384
|
|
|
206
385
|
gateway: {
|
|
@@ -232,7 +411,14 @@ export const bitrix24Plugin = {
|
|
|
232
411
|
const sendService = new SendService(api, logger);
|
|
233
412
|
|
|
234
413
|
// Register or update bot on the B24 portal
|
|
235
|
-
await ensureBotRegistered(api, config, logger);
|
|
414
|
+
const botId = await ensureBotRegistered(api, config, logger);
|
|
415
|
+
|
|
416
|
+
// Register slash commands (runs in background, doesn't block startup)
|
|
417
|
+
if (botId) {
|
|
418
|
+
ensureCommandsRegistered(api, config, botId, logger).catch((err) => {
|
|
419
|
+
logger.warn('Command registration failed', err);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
236
422
|
|
|
237
423
|
const inboundHandler = new InboundHandler({
|
|
238
424
|
config,
|
|
@@ -284,7 +470,7 @@ export const bitrix24Plugin = {
|
|
|
284
470
|
MessageSid: msgCtx.messageId,
|
|
285
471
|
Timestamp: Date.now(),
|
|
286
472
|
WasMentioned: false,
|
|
287
|
-
CommandAuthorized:
|
|
473
|
+
CommandAuthorized: true,
|
|
288
474
|
OriginatingChannel: 'bitrix24',
|
|
289
475
|
OriginatingTo: `bitrix24:${msgCtx.chatId}`,
|
|
290
476
|
});
|
|
@@ -304,7 +490,8 @@ export const bitrix24Plugin = {
|
|
|
304
490
|
dispatcherOptions: {
|
|
305
491
|
deliver: async (payload) => {
|
|
306
492
|
if (payload.text) {
|
|
307
|
-
|
|
493
|
+
const keyboard = extractKeyboardFromPayload(payload);
|
|
494
|
+
await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
|
|
308
495
|
}
|
|
309
496
|
},
|
|
310
497
|
onReplyStart: async () => {
|
|
@@ -322,6 +509,103 @@ export const bitrix24Plugin = {
|
|
|
322
509
|
}
|
|
323
510
|
},
|
|
324
511
|
|
|
512
|
+
onCommand: async (event: B24CommandEvent) => {
|
|
513
|
+
const cmdEntry = Object.values(event.data.COMMAND)[0];
|
|
514
|
+
if (!cmdEntry) {
|
|
515
|
+
logger.warn('No command entry in ONIMCOMMANDADD event');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const commandName = cmdEntry.COMMAND;
|
|
520
|
+
const commandParams = cmdEntry.COMMAND_PARAMS?.trim() ?? '';
|
|
521
|
+
const commandText = commandParams
|
|
522
|
+
? `/${commandName} ${commandParams}`
|
|
523
|
+
: `/${commandName}`;
|
|
524
|
+
|
|
525
|
+
const senderId = String(event.data.PARAMS.FROM_USER_ID);
|
|
526
|
+
const dialogId = event.data.PARAMS.DIALOG_ID;
|
|
527
|
+
const isDm = event.data.PARAMS.CHAT_TYPE === 'P';
|
|
528
|
+
const user = event.data.USER;
|
|
529
|
+
|
|
530
|
+
logger.info('Inbound command', { commandName, commandParams, senderId, dialogId });
|
|
531
|
+
|
|
532
|
+
// Access control
|
|
533
|
+
if (!checkAccess(senderId, config)) {
|
|
534
|
+
logger.debug(`Access denied for command from user ${senderId}`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const runtime = getBitrix24Runtime();
|
|
539
|
+
const cfg = runtime.config.loadConfig();
|
|
540
|
+
|
|
541
|
+
const route = runtime.channel.routing.resolveAgentRoute({
|
|
542
|
+
cfg,
|
|
543
|
+
channel: 'bitrix24',
|
|
544
|
+
accountId: ctx.accountId,
|
|
545
|
+
peer: { kind: isDm ? 'direct' : 'group', id: dialogId },
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Native commands use a separate slash-command session (like Telegram)
|
|
549
|
+
const slashSessionKey = `bitrix24:slash:${senderId}`;
|
|
550
|
+
|
|
551
|
+
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
552
|
+
Body: commandText,
|
|
553
|
+
BodyForAgent: commandText,
|
|
554
|
+
RawBody: commandText,
|
|
555
|
+
CommandBody: commandText,
|
|
556
|
+
CommandAuthorized: true,
|
|
557
|
+
CommandSource: 'native',
|
|
558
|
+
CommandTargetSessionKey: route.sessionKey,
|
|
559
|
+
From: `bitrix24:${dialogId}`,
|
|
560
|
+
To: `slash:${senderId}`,
|
|
561
|
+
SessionKey: slashSessionKey,
|
|
562
|
+
AccountId: route.accountId,
|
|
563
|
+
ChatType: isDm ? 'direct' : 'group',
|
|
564
|
+
ConversationLabel: user.NAME,
|
|
565
|
+
SenderName: user.NAME,
|
|
566
|
+
SenderId: senderId,
|
|
567
|
+
Provider: 'bitrix24',
|
|
568
|
+
Surface: 'bitrix24',
|
|
569
|
+
MessageSid: String(event.data.PARAMS.MESSAGE_ID),
|
|
570
|
+
Timestamp: Date.now(),
|
|
571
|
+
WasMentioned: true,
|
|
572
|
+
OriginatingChannel: 'bitrix24',
|
|
573
|
+
OriginatingTo: `bitrix24:${dialogId}`,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const sendCtx = {
|
|
577
|
+
webhookUrl: config.webhookUrl,
|
|
578
|
+
clientEndpoint: (cmdEntry.client_endpoint as string | undefined) ?? event.auth.client_endpoint,
|
|
579
|
+
botToken: cmdEntry.access_token,
|
|
580
|
+
dialogId,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
585
|
+
ctx: inboundCtx,
|
|
586
|
+
cfg,
|
|
587
|
+
dispatcherOptions: {
|
|
588
|
+
deliver: async (payload) => {
|
|
589
|
+
if (payload.text) {
|
|
590
|
+
const keyboard = extractKeyboardFromPayload(payload);
|
|
591
|
+
await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
onReplyStart: async () => {
|
|
595
|
+
if (config.showTyping !== false) {
|
|
596
|
+
await sendService.sendTyping(sendCtx);
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
onError: (err) => {
|
|
600
|
+
logger.error('Error delivering command reply to B24', err);
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
} catch (err) {
|
|
605
|
+
logger.error('Error dispatching command to agent', err);
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
|
|
325
609
|
onJoinChat: async (event: B24JoinChatEvent) => {
|
|
326
610
|
logger.info('Bot joined chat', {
|
|
327
611
|
dialogId: event.data.PARAMS.DIALOG_ID,
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw bot commands to register with Bitrix24.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the native commands registered by the Telegram plugin via setMyCommands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface BotCommandDef {
|
|
8
|
+
command: string;
|
|
9
|
+
en: string;
|
|
10
|
+
ru: string;
|
|
11
|
+
params?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Standard OpenClaw commands.
|
|
16
|
+
* Excludes: focus/unfocus/agents (Discord-specific), allowlist/bash (text-only scope).
|
|
17
|
+
*/
|
|
18
|
+
export const OPENCLAW_COMMANDS: BotCommandDef[] = [
|
|
19
|
+
// ── Status ──
|
|
20
|
+
{ command: 'help', en: 'Show available commands', ru: 'Показать доступные команды' },
|
|
21
|
+
{ command: 'commands', en: 'List all slash commands', ru: 'Список всех команд' },
|
|
22
|
+
{ command: 'status', en: 'Show current status', ru: 'Показать текущий статус' },
|
|
23
|
+
{ command: 'context', en: 'Explain how context is built', ru: 'Объяснить построение контекста' },
|
|
24
|
+
{ command: 'whoami', en: 'Show your sender ID', ru: 'Показать ваш ID' },
|
|
25
|
+
{ command: 'usage', en: 'Usage and cost summary', ru: 'Использование и стоимость', params: 'off|tokens|full|cost' },
|
|
26
|
+
|
|
27
|
+
// ── Session ──
|
|
28
|
+
{ command: 'new', en: 'Start a new session', ru: 'Начать новую сессию' },
|
|
29
|
+
{ command: 'reset', en: 'Reset the current session', ru: 'Сбросить текущую сессию' },
|
|
30
|
+
{ command: 'stop', en: 'Stop the current run', ru: 'Остановить текущий запуск' },
|
|
31
|
+
{ command: 'compact', en: 'Compact the session context', ru: 'Сжать контекст сессии', params: 'instructions' },
|
|
32
|
+
{ command: 'session', en: 'Manage session settings', ru: 'Настройки сессии', params: 'ttl|...' },
|
|
33
|
+
|
|
34
|
+
// ── Options ──
|
|
35
|
+
{ command: 'model', en: 'Show or set the model', ru: 'Показать/сменить модель', params: 'model name' },
|
|
36
|
+
{ command: 'models', en: 'List available models', ru: 'Список моделей', params: 'provider' },
|
|
37
|
+
{ command: 'think', en: 'Set thinking level', ru: 'Уровень размышлений', params: 'off|low|medium|high' },
|
|
38
|
+
{ command: 'verbose', en: 'Toggle verbose mode', ru: 'Подробный режим', params: 'on|off' },
|
|
39
|
+
{ command: 'reasoning', en: 'Toggle reasoning visibility', ru: 'Видимость рассуждений', params: 'on|off|stream' },
|
|
40
|
+
{ command: 'elevated', en: 'Toggle elevated mode', ru: 'Режим с расширенными правами', params: 'on|off|ask|full' },
|
|
41
|
+
{ command: 'exec', en: 'Set exec defaults', ru: 'Настройки выполнения', params: 'host|security|ask|node' },
|
|
42
|
+
{ command: 'queue', en: 'Adjust queue settings', ru: 'Настройки очереди', params: 'mode|debounce|cap|drop' },
|
|
43
|
+
|
|
44
|
+
// ── Management ──
|
|
45
|
+
{ command: 'config', en: 'Show or set config values', ru: 'Показать/задать конфигурацию', params: 'show|get|set|unset' },
|
|
46
|
+
{ command: 'debug', en: 'Set runtime debug overrides', ru: 'Отладочные настройки', params: 'show|reset|set|unset' },
|
|
47
|
+
{ command: 'approve', en: 'Approve or deny exec requests', ru: 'Одобрить/отклонить запросы' },
|
|
48
|
+
{ command: 'activation', en: 'Set group activation mode', ru: 'Режим активации в группах', params: 'mention|always' },
|
|
49
|
+
{ command: 'send', en: 'Set send policy', ru: 'Политика отправки', params: 'on|off|inherit' },
|
|
50
|
+
{ command: 'subagents', en: 'Manage subagent runs', ru: 'Управление субагентами' },
|
|
51
|
+
{ command: 'kill', en: 'Kill a running subagent', ru: 'Остановить субагента', params: 'id|all' },
|
|
52
|
+
{ command: 'steer', en: 'Send guidance to a subagent', ru: 'Направить субагента', params: 'message' },
|
|
53
|
+
|
|
54
|
+
// ── Tools ──
|
|
55
|
+
{ command: 'skill', en: 'Run a skill by name', ru: 'Запустить навык', params: 'name' },
|
|
56
|
+
{ command: 'restart', en: 'Restart OpenClaw', ru: 'Перезапустить OpenClaw' },
|
|
57
|
+
|
|
58
|
+
// ── Export ──
|
|
59
|
+
{ command: 'export-session', en: 'Export session to HTML', ru: 'Экспорт сессии в HTML' },
|
|
60
|
+
];
|
package/src/config-schema.ts
CHANGED
|
@@ -6,7 +6,6 @@ const AccountSchema = z.object({
|
|
|
6
6
|
botName: z.string().optional().default('OpenClaw'),
|
|
7
7
|
botCode: z.string().optional().default('openclaw'),
|
|
8
8
|
botAvatar: z.string().optional(),
|
|
9
|
-
callbackPath: z.string().optional().default('/hooks/bitrix24'),
|
|
10
9
|
callbackUrl: z.string().url().optional(),
|
|
11
10
|
dmPolicy: z.enum(['open', 'allowlist', 'pairing']).optional().default('open'),
|
|
12
11
|
allowFrom: z.array(z.string()).optional(),
|
package/src/message-utils.ts
CHANGED
|
@@ -90,15 +90,25 @@ export function buildKeyboard(
|
|
|
90
90
|
}>
|
|
91
91
|
>,
|
|
92
92
|
): B24Keyboard {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
const keyboard: B24Keyboard = [];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < rows.length; i++) {
|
|
96
|
+
for (const btn of rows[i]) {
|
|
97
|
+
keyboard.push({
|
|
98
|
+
TEXT: btn.text,
|
|
99
|
+
...(btn.command ? { COMMAND: btn.command, COMMAND_PARAMS: btn.commandParams ?? '' } : {}),
|
|
100
|
+
...(btn.link ? { LINK: btn.link } : {}),
|
|
101
|
+
BG_COLOR: btn.bgColor ?? '#29619b',
|
|
102
|
+
TEXT_COLOR: btn.textColor ?? '#fff',
|
|
103
|
+
DISPLAY: btn.fullWidth ? 'LINE' : 'BLOCK',
|
|
104
|
+
BLOCK: btn.disableAfterClick ? 'Y' : 'N',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (i < rows.length - 1) {
|
|
109
|
+
keyboard.push({ TYPE: 'NEWLINE' });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return keyboard;
|
|
104
114
|
}
|
package/src/types.ts
CHANGED
|
@@ -211,14 +211,23 @@ export interface KeyboardButton {
|
|
|
211
211
|
COMMAND?: string;
|
|
212
212
|
COMMAND_PARAMS?: string;
|
|
213
213
|
BG_COLOR?: string;
|
|
214
|
+
BG_COLOR_TOKEN?: 'primary' | 'secondary' | 'alert' | 'base';
|
|
214
215
|
TEXT_COLOR?: string;
|
|
215
216
|
DISPLAY?: 'LINE' | 'BLOCK';
|
|
216
217
|
DISABLED?: 'Y' | 'N';
|
|
217
218
|
BLOCK?: 'Y' | 'N';
|
|
218
219
|
LINK?: string;
|
|
220
|
+
WIDTH?: number;
|
|
221
|
+
ACTION?: 'PUT' | 'SEND' | 'COPY' | 'CALL' | 'DIALOG';
|
|
222
|
+
ACTION_VALUE?: string;
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
export
|
|
225
|
+
export interface KeyboardNewline {
|
|
226
|
+
TYPE: 'NEWLINE';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** B24 keyboard: flat array with NEWLINE separators between rows */
|
|
230
|
+
export type B24Keyboard = (KeyboardButton | KeyboardNewline)[];
|
|
222
231
|
|
|
223
232
|
export interface SendMessageOptions {
|
|
224
233
|
ATTACH?: unknown;
|
|
@@ -242,7 +251,6 @@ export interface Bitrix24AccountConfig {
|
|
|
242
251
|
botName?: string;
|
|
243
252
|
botCode?: string;
|
|
244
253
|
botAvatar?: string;
|
|
245
|
-
callbackPath?: string;
|
|
246
254
|
callbackUrl?: string;
|
|
247
255
|
dmPolicy?: 'open' | 'allowlist' | 'pairing';
|
|
248
256
|
allowFrom?: string[];
|
|
@@ -78,18 +78,16 @@ describe('splitMessage', () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
describe('buildKeyboard', () => {
|
|
81
|
-
it('builds a single row keyboard', () => {
|
|
81
|
+
it('builds a single row keyboard (flat array)', () => {
|
|
82
82
|
const kb = buildKeyboard([
|
|
83
83
|
[{ text: 'Yes', command: 'answer', commandParams: 'yes' }],
|
|
84
84
|
]);
|
|
85
|
+
// Flat array: 1 button, no NEWLINE
|
|
85
86
|
expect(kb).toHaveLength(1);
|
|
86
|
-
expect(kb[0]).
|
|
87
|
-
expect(kb[0][0].TEXT).toBe('Yes');
|
|
88
|
-
expect(kb[0][0].COMMAND).toBe('answer');
|
|
89
|
-
expect(kb[0][0].COMMAND_PARAMS).toBe('yes');
|
|
87
|
+
expect(kb[0]).toMatchObject({ TEXT: 'Yes', COMMAND: 'answer', COMMAND_PARAMS: 'yes' });
|
|
90
88
|
});
|
|
91
89
|
|
|
92
|
-
it('builds multi-row keyboard', () => {
|
|
90
|
+
it('builds multi-row keyboard with NEWLINE separators', () => {
|
|
93
91
|
const kb = buildKeyboard([
|
|
94
92
|
[
|
|
95
93
|
{ text: 'Yes', command: 'answer', commandParams: 'yes' },
|
|
@@ -97,27 +95,26 @@ describe('buildKeyboard', () => {
|
|
|
97
95
|
],
|
|
98
96
|
[{ text: 'More info', link: 'https://example.com', fullWidth: true }],
|
|
99
97
|
]);
|
|
100
|
-
|
|
101
|
-
expect(kb
|
|
102
|
-
expect(kb[
|
|
103
|
-
expect(kb[1]
|
|
104
|
-
expect(kb[
|
|
98
|
+
// Flat: [btn, btn, NEWLINE, btn] = 4 items
|
|
99
|
+
expect(kb).toHaveLength(4);
|
|
100
|
+
expect(kb[0]).toMatchObject({ TEXT: 'Yes' });
|
|
101
|
+
expect(kb[1]).toMatchObject({ TEXT: 'No' });
|
|
102
|
+
expect(kb[2]).toMatchObject({ TYPE: 'NEWLINE' });
|
|
103
|
+
expect(kb[3]).toMatchObject({ TEXT: 'More info', LINK: 'https://example.com', DISPLAY: 'LINE' });
|
|
105
104
|
});
|
|
106
105
|
|
|
107
106
|
it('applies default colors', () => {
|
|
108
107
|
const kb = buildKeyboard([[{ text: 'Click' }]]);
|
|
109
|
-
expect(kb[0]
|
|
110
|
-
expect(kb[0][0].TEXT_COLOR).toBe('#fff');
|
|
108
|
+
expect(kb[0]).toMatchObject({ BG_COLOR: '#29619b', TEXT_COLOR: '#fff' });
|
|
111
109
|
});
|
|
112
110
|
|
|
113
111
|
it('applies custom colors', () => {
|
|
114
112
|
const kb = buildKeyboard([[{ text: 'Click', bgColor: '#333', textColor: '#eee' }]]);
|
|
115
|
-
expect(kb[0]
|
|
116
|
-
expect(kb[0][0].TEXT_COLOR).toBe('#eee');
|
|
113
|
+
expect(kb[0]).toMatchObject({ BG_COLOR: '#333', TEXT_COLOR: '#eee' });
|
|
117
114
|
});
|
|
118
115
|
|
|
119
116
|
it('sets BLOCK=Y when disableAfterClick is true', () => {
|
|
120
117
|
const kb = buildKeyboard([[{ text: 'Once', command: 'once', disableAfterClick: true }]]);
|
|
121
|
-
expect(kb[0]
|
|
118
|
+
expect(kb[0]).toMatchObject({ BLOCK: 'Y' });
|
|
122
119
|
});
|
|
123
120
|
});
|