@ihazz/bitrix24 0.1.4 → 0.1.6

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
@@ -1,12 +1,25 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { basename } from 'node:path';
2
3
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
4
  import { listAccountIds, resolveAccount, getConfig } from './config.js';
4
5
  import { Bitrix24Api } from './api.js';
5
6
  import { SendService } from './send-service.js';
7
+ import { MediaService } from './media-service.js';
8
+ import type { DownloadedMedia } from './media-service.js';
6
9
  import { InboundHandler } from './inbound-handler.js';
10
+ import { normalizeAllowEntry, checkAccessWithPairing } from './access-control.js';
7
11
  import { defaultLogger } from './utils.js';
8
12
  import { getBitrix24Runtime } from './runtime.js';
9
- import type { B24MsgContext, B24JoinChatEvent, Bitrix24AccountConfig } from './types.js';
13
+ import type { ChannelPairingAdapter } from './runtime.js';
14
+ import { OPENCLAW_COMMANDS } from './commands.js';
15
+ import type {
16
+ B24MsgContext,
17
+ B24JoinChatEvent,
18
+ B24CommandEvent,
19
+ Bitrix24AccountConfig,
20
+ B24Keyboard,
21
+ KeyboardButton,
22
+ } from './types.js';
10
23
 
11
24
  interface Logger {
12
25
  info: (...args: unknown[]) => void;
@@ -19,11 +32,89 @@ interface Logger {
19
32
  interface GatewayState {
20
33
  api: Bitrix24Api;
21
34
  sendService: SendService;
35
+ mediaService: MediaService;
22
36
  inboundHandler: InboundHandler;
23
37
  }
24
38
 
25
39
  let gatewayState: GatewayState | null = null;
26
40
 
41
+ // ─── Keyboard / Button conversion ────────────────────────────────────────────
42
+
43
+ /** Generic button format used by OpenClaw channelData. */
44
+ export interface ChannelButton {
45
+ text: string;
46
+ callback_data?: string;
47
+ style?: string;
48
+ }
49
+
50
+ /**
51
+ * Convert OpenClaw button rows to B24 flat KEYBOARD array.
52
+ * Input: Array<Array<{ text, callback_data, style }>>
53
+ * Output: flat array with { TYPE: 'NEWLINE' } separators between rows.
54
+ */
55
+ export function convertButtonsToKeyboard(rows: ChannelButton[][]): B24Keyboard {
56
+ const keyboard: B24Keyboard = [];
57
+
58
+ for (let i = 0; i < rows.length; i++) {
59
+ for (const btn of rows[i]) {
60
+ const b24Btn: KeyboardButton = { TEXT: btn.text, DISPLAY: 'LINE' };
61
+
62
+ if (btn.callback_data?.startsWith('/')) {
63
+ // Slash command — use COMMAND + COMMAND_PARAMS
64
+ const parts = btn.callback_data.substring(1).split(' ');
65
+ b24Btn.COMMAND = parts[0];
66
+ if (parts.length > 1) {
67
+ b24Btn.COMMAND_PARAMS = parts.slice(1).join(' ');
68
+ }
69
+ } else if (btn.callback_data) {
70
+ // Non-slash data — insert text into input via PUT action
71
+ b24Btn.ACTION = 'PUT';
72
+ b24Btn.ACTION_VALUE = btn.callback_data;
73
+ }
74
+
75
+ if (btn.style === 'primary') {
76
+ b24Btn.BG_COLOR_TOKEN = 'primary';
77
+ } else if (btn.style === 'attention' || btn.style === 'danger') {
78
+ b24Btn.BG_COLOR_TOKEN = 'alert';
79
+ }
80
+
81
+ keyboard.push(b24Btn);
82
+ }
83
+
84
+ // Add NEWLINE separator between rows (not after last row)
85
+ if (i < rows.length - 1) {
86
+ keyboard.push({ TYPE: 'NEWLINE' });
87
+ }
88
+ }
89
+
90
+ return keyboard;
91
+ }
92
+
93
+ /**
94
+ * Extract B24 keyboard from a dispatcher payload's channelData.
95
+ * Checks bitrix24-specific data first, then falls back to OpenClaw generic button format.
96
+ */
97
+ export function extractKeyboardFromPayload(
98
+ payload: { channelData?: Record<string, unknown> },
99
+ ): B24Keyboard | undefined {
100
+ const cd = payload.channelData;
101
+ if (!cd) return undefined;
102
+
103
+ // Direct B24 keyboard (future-proof: channelData.bitrix24.keyboard)
104
+ const b24Data = cd.bitrix24 as { keyboard?: B24Keyboard } | undefined;
105
+ if (b24Data?.keyboard?.length) {
106
+ return b24Data.keyboard;
107
+ }
108
+
109
+ // Translate from OpenClaw generic button format (channelData.telegram key)
110
+ const tgData = cd.telegram as { buttons?: ChannelButton[][] } | undefined;
111
+ if (tgData?.buttons?.length) {
112
+ return convertButtonsToKeyboard(tgData.buttons);
113
+ }
114
+
115
+ return undefined;
116
+ }
117
+
27
118
  /**
28
119
  * Register or update the bot on the Bitrix24 portal.
29
120
  * Checks imbot.bot.list first; if a bot with the same CODE exists, updates it.
@@ -82,6 +173,51 @@ async function ensureBotRegistered(
82
173
  }
83
174
  }
84
175
 
176
+ /**
177
+ * Register OpenClaw slash commands with the B24 bot.
178
+ * Runs in the background — errors are logged but don't block startup.
179
+ */
180
+ async function ensureCommandsRegistered(
181
+ api: Bitrix24Api,
182
+ config: Bitrix24AccountConfig,
183
+ botId: number,
184
+ logger: Logger,
185
+ ): Promise<void> {
186
+ const { webhookUrl, callbackUrl } = config;
187
+ if (!webhookUrl || !callbackUrl) return;
188
+
189
+ let registered = 0;
190
+ let skipped = 0;
191
+
192
+ for (const cmd of OPENCLAW_COMMANDS) {
193
+ try {
194
+ await api.registerCommand(webhookUrl, {
195
+ BOT_ID: botId,
196
+ COMMAND: cmd.command,
197
+ COMMON: 'N',
198
+ HIDDEN: 'N',
199
+ EXTRANET_SUPPORT: 'N',
200
+ LANG: [
201
+ { LANGUAGE_ID: 'en', TITLE: cmd.en, ...(cmd.params ? { PARAMS: cmd.params } : {}) },
202
+ { LANGUAGE_ID: 'ru', TITLE: cmd.ru, ...(cmd.params ? { PARAMS: cmd.params } : {}) },
203
+ ],
204
+ EVENT_COMMAND_ADD: callbackUrl,
205
+ });
206
+ registered++;
207
+ } catch (err: unknown) {
208
+ // "WRONG_REQUEST" typically means command already exists
209
+ const msg = err instanceof Error ? err.message : String(err);
210
+ if (msg.includes('WRONG_REQUEST') || msg.includes('already')) {
211
+ skipped++;
212
+ } else {
213
+ logger.warn(`Failed to register command /${cmd.command}`, err);
214
+ }
215
+ }
216
+ }
217
+
218
+ logger.info(`Commands sync: ${registered} registered, ${skipped} already existed (total ${OPENCLAW_COMMANDS.length})`);
219
+ }
220
+
85
221
  /**
86
222
  * Handle an incoming HTTP request on the webhook route.
87
223
  * Called by the HTTP route registered in index.ts.
@@ -140,7 +276,7 @@ export const bitrix24Plugin = {
140
276
 
141
277
  capabilities: {
142
278
  chatTypes: ['direct', 'group'] as const,
143
- media: false,
279
+ media: true,
144
280
  reactions: false,
145
281
  threads: false,
146
282
  nativeCommands: true,
@@ -153,12 +289,33 @@ export const bitrix24Plugin = {
153
289
  },
154
290
 
155
291
  security: {
156
- resolveDmPolicy: (account: { config?: { dmPolicy?: string } }) =>
157
- account.config?.dmPolicy ?? 'open',
292
+ resolveDmPolicy: (params: { cfg?: Record<string, unknown>; accountId?: string; account?: { config?: Record<string, unknown> } }) => ({
293
+ policy: (params.account?.config?.dmPolicy as string) ?? 'pairing',
294
+ allowFrom: (params.account?.config?.allowFrom as string[]) ?? [],
295
+ policyPath: 'channels.bitrix24.dmPolicy',
296
+ allowFromPath: 'channels.bitrix24.',
297
+ approveHint: 'openclaw pairing approve bitrix24 <CODE>',
298
+ normalizeEntry: (raw: string) => raw.replace(/^(bitrix24|b24|bx24):/i, ''),
299
+ }),
158
300
  normalizeAllowFrom: (entry: string) =>
159
- entry.replace(/^(bitrix24|b24|bx24):/, ''),
301
+ entry.replace(/^(bitrix24|b24|bx24):/i, ''),
160
302
  },
161
303
 
304
+ pairing: {
305
+ idLabel: 'bitrix24UserId',
306
+ normalizeAllowEntry: (entry: string) => normalizeAllowEntry(entry),
307
+ notifyApproval: async (params: { cfg: Record<string, unknown>; id: string; runtime?: unknown }) => {
308
+ const { config: acctCfg } = resolveAccount(params.cfg);
309
+ if (!acctCfg.webhookUrl) return;
310
+ const api = new Bitrix24Api();
311
+ try {
312
+ await api.sendMessage(acctCfg.webhookUrl, params.id, '\u2705 OpenClaw access approved.');
313
+ } finally {
314
+ api.destroy();
315
+ }
316
+ },
317
+ } satisfies ChannelPairingAdapter,
318
+
162
319
  outbound: {
163
320
  deliveryMode: 'direct' as const,
164
321
 
@@ -201,6 +358,55 @@ export const bitrix24Plugin = {
201
358
  error: result.error,
202
359
  };
203
360
  },
361
+
362
+ /**
363
+ * Send a payload with optional channelData (keyboards, etc.) to B24.
364
+ * Called by OpenClaw when the response includes channelData.
365
+ */
366
+ sendPayload: async (params: {
367
+ text: string;
368
+ channelData?: Record<string, unknown>;
369
+ context: B24MsgContext;
370
+ account: { config: { webhookUrl?: string; showTyping?: boolean } };
371
+ }) => {
372
+ const { text, channelData, context, account } = params;
373
+
374
+ if (!gatewayState) {
375
+ return { ok: false, error: 'Gateway not started', channel: 'bitrix24' };
376
+ }
377
+
378
+ const { sendService } = gatewayState;
379
+
380
+ const sendCtx = {
381
+ webhookUrl: account.config.webhookUrl,
382
+ clientEndpoint: context.clientEndpoint,
383
+ botToken: context.botToken,
384
+ dialogId: context.chatId,
385
+ };
386
+
387
+ // Send typing indicator
388
+ if (account.config.showTyping !== false) {
389
+ await sendService.sendTyping(sendCtx);
390
+ }
391
+
392
+ // Extract keyboard from channelData
393
+ const keyboard = channelData
394
+ ? extractKeyboardFromPayload({ channelData })
395
+ : undefined;
396
+
397
+ const result = await sendService.sendText(
398
+ sendCtx,
399
+ text,
400
+ keyboard ? { keyboard } : undefined,
401
+ );
402
+
403
+ return {
404
+ ok: result.ok,
405
+ messageId: result.messageId,
406
+ channel: 'bitrix24' as const,
407
+ error: result.error,
408
+ };
409
+ },
204
410
  },
205
411
 
206
412
  gateway: {
@@ -230,9 +436,17 @@ export const bitrix24Plugin = {
230
436
  const clientId = createHash('md5').update(config.webhookUrl).digest('hex');
231
437
  const api = new Bitrix24Api({ logger, clientId });
232
438
  const sendService = new SendService(api, logger);
439
+ const mediaService = new MediaService(api, logger);
233
440
 
234
441
  // Register or update bot on the B24 portal
235
- await ensureBotRegistered(api, config, logger);
442
+ const botId = await ensureBotRegistered(api, config, logger);
443
+
444
+ // Register slash commands (runs in background, doesn't block startup)
445
+ if (botId) {
446
+ ensureCommandsRegistered(api, config, botId, logger).catch((err) => {
447
+ logger.warn('Command registration failed', err);
448
+ });
449
+ }
236
450
 
237
451
  const inboundHandler = new InboundHandler({
238
452
  config,
@@ -249,6 +463,64 @@ export const bitrix24Plugin = {
249
463
  const runtime = getBitrix24Runtime();
250
464
  const cfg = runtime.config.loadConfig();
251
465
 
466
+ // Pairing-aware access control
467
+ const accessResult = await checkAccessWithPairing({
468
+ senderId: msgCtx.senderId,
469
+ config,
470
+ runtime,
471
+ accountId: ctx.accountId,
472
+ pairingAdapter: bitrix24Plugin.pairing,
473
+ sendReply: async (text: string) => {
474
+ const replySendCtx = {
475
+ webhookUrl: config.webhookUrl,
476
+ clientEndpoint: msgCtx.clientEndpoint,
477
+ botToken: msgCtx.botToken,
478
+ dialogId: msgCtx.chatId,
479
+ };
480
+ await sendService.sendText(replySendCtx, text, { convertMarkdown: false });
481
+ },
482
+ logger,
483
+ });
484
+
485
+ if (accessResult !== 'allow') {
486
+ logger.debug(`Message blocked (${accessResult})`, { senderId: msgCtx.senderId });
487
+ return;
488
+ }
489
+
490
+ // Download media files if present
491
+ let mediaFields: Record<string, unknown> = {};
492
+ if (msgCtx.media.length > 0) {
493
+ const downloaded = (await Promise.all(
494
+ msgCtx.media.map((m) =>
495
+ mediaService.downloadMedia({
496
+ fileId: m.id,
497
+ fileName: m.name,
498
+ extension: m.extension,
499
+ clientEndpoint: msgCtx.clientEndpoint,
500
+ userToken: msgCtx.userToken,
501
+ }),
502
+ ),
503
+ )).filter(Boolean) as DownloadedMedia[];
504
+
505
+ if (downloaded.length > 0) {
506
+ mediaFields = {
507
+ MediaPath: downloaded[0].path,
508
+ MediaType: downloaded[0].contentType,
509
+ MediaUrl: downloaded[0].path,
510
+ MediaPaths: downloaded.map((m) => m.path),
511
+ MediaUrls: downloaded.map((m) => m.path),
512
+ MediaTypes: downloaded.map((m) => m.contentType),
513
+ };
514
+ }
515
+ }
516
+
517
+ // Use placeholder body for media-only messages
518
+ let body = msgCtx.text;
519
+ if (!body && msgCtx.media.length > 0) {
520
+ const hasImage = msgCtx.media.some((m) => m.type === 'image');
521
+ body = hasImage ? '<media:image>' : '<media:document>';
522
+ }
523
+
252
524
  // Resolve which agent handles this conversation
253
525
  const route = runtime.channel.routing.resolveAgentRoute({
254
526
  cfg,
@@ -268,9 +540,9 @@ export const bitrix24Plugin = {
268
540
 
269
541
  // Build and finalize inbound context for OpenClaw agent
270
542
  const inboundCtx = runtime.channel.reply.finalizeInboundContext({
271
- Body: msgCtx.text,
272
- BodyForAgent: msgCtx.text,
273
- RawBody: msgCtx.text,
543
+ Body: body,
544
+ BodyForAgent: body,
545
+ RawBody: body,
274
546
  From: `bitrix24:${msgCtx.chatId}`,
275
547
  To: `bitrix24:${msgCtx.chatId}`,
276
548
  SessionKey: route.sessionKey,
@@ -284,9 +556,10 @@ export const bitrix24Plugin = {
284
556
  MessageSid: msgCtx.messageId,
285
557
  Timestamp: Date.now(),
286
558
  WasMentioned: false,
287
- CommandAuthorized: false,
559
+ CommandAuthorized: true,
288
560
  OriginatingChannel: 'bitrix24',
289
561
  OriginatingTo: `bitrix24:${msgCtx.chatId}`,
562
+ ...mediaFields,
290
563
  });
291
564
 
292
565
  const sendCtx = {
@@ -303,8 +576,21 @@ export const bitrix24Plugin = {
303
576
  cfg,
304
577
  dispatcherOptions: {
305
578
  deliver: async (payload) => {
579
+ // Send media if present in reply
580
+ const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
581
+ for (const mediaUrl of mediaUrls) {
582
+ await mediaService.uploadMediaToChat({
583
+ localPath: mediaUrl,
584
+ fileName: basename(mediaUrl),
585
+ chatId: Number(msgCtx.chatInternalId),
586
+ clientEndpoint: msgCtx.clientEndpoint,
587
+ botToken: msgCtx.botToken,
588
+ });
589
+ }
590
+ // Send text if present
306
591
  if (payload.text) {
307
- await sendService.sendText(sendCtx, payload.text);
592
+ const keyboard = extractKeyboardFromPayload(payload);
593
+ await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
308
594
  }
309
595
  },
310
596
  onReplyStart: async () => {
@@ -322,6 +608,113 @@ export const bitrix24Plugin = {
322
608
  }
323
609
  },
324
610
 
611
+ onCommand: async (event: B24CommandEvent) => {
612
+ const cmdEntry = Object.values(event.data.COMMAND)[0];
613
+ if (!cmdEntry) {
614
+ logger.warn('No command entry in ONIMCOMMANDADD event');
615
+ return;
616
+ }
617
+
618
+ const commandName = cmdEntry.COMMAND;
619
+ const commandParams = cmdEntry.COMMAND_PARAMS?.trim() ?? '';
620
+ const commandText = commandParams
621
+ ? `/${commandName} ${commandParams}`
622
+ : `/${commandName}`;
623
+
624
+ const senderId = String(event.data.PARAMS.FROM_USER_ID);
625
+ const dialogId = event.data.PARAMS.DIALOG_ID;
626
+ const isDm = event.data.PARAMS.CHAT_TYPE === 'P';
627
+ const user = event.data.USER;
628
+
629
+ logger.info('Inbound command', { commandName, commandParams, senderId, dialogId });
630
+
631
+ const runtime = getBitrix24Runtime();
632
+ const cfg = runtime.config.loadConfig();
633
+
634
+ // 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
+ });
644
+
645
+ if (accessResult !== 'allow') {
646
+ logger.debug(`Command blocked (${accessResult})`, { senderId });
647
+ return;
648
+ }
649
+
650
+ const route = runtime.channel.routing.resolveAgentRoute({
651
+ cfg,
652
+ channel: 'bitrix24',
653
+ accountId: ctx.accountId,
654
+ peer: { kind: isDm ? 'direct' : 'group', id: dialogId },
655
+ });
656
+
657
+ // Native commands use a separate slash-command session
658
+ const slashSessionKey = `bitrix24:slash:${senderId}`;
659
+
660
+ const inboundCtx = runtime.channel.reply.finalizeInboundContext({
661
+ Body: commandText,
662
+ BodyForAgent: commandText,
663
+ RawBody: commandText,
664
+ CommandBody: commandText,
665
+ CommandAuthorized: true,
666
+ CommandSource: 'native',
667
+ CommandTargetSessionKey: route.sessionKey,
668
+ From: `bitrix24:${dialogId}`,
669
+ To: `slash:${senderId}`,
670
+ SessionKey: slashSessionKey,
671
+ AccountId: route.accountId,
672
+ ChatType: isDm ? 'direct' : 'group',
673
+ ConversationLabel: user.NAME,
674
+ SenderName: user.NAME,
675
+ SenderId: senderId,
676
+ Provider: 'bitrix24',
677
+ Surface: 'bitrix24',
678
+ MessageSid: String(event.data.PARAMS.MESSAGE_ID),
679
+ Timestamp: Date.now(),
680
+ WasMentioned: true,
681
+ OriginatingChannel: 'bitrix24',
682
+ OriginatingTo: `bitrix24:${dialogId}`,
683
+ });
684
+
685
+ const sendCtx = {
686
+ webhookUrl: config.webhookUrl,
687
+ clientEndpoint: (cmdEntry.client_endpoint as string | undefined) ?? event.auth.client_endpoint,
688
+ botToken: cmdEntry.access_token,
689
+ dialogId,
690
+ };
691
+
692
+ try {
693
+ await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
694
+ ctx: inboundCtx,
695
+ cfg,
696
+ dispatcherOptions: {
697
+ deliver: async (payload) => {
698
+ if (payload.text) {
699
+ const keyboard = extractKeyboardFromPayload(payload);
700
+ await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
701
+ }
702
+ },
703
+ onReplyStart: async () => {
704
+ if (config.showTyping !== false) {
705
+ await sendService.sendTyping(sendCtx);
706
+ }
707
+ },
708
+ onError: (err) => {
709
+ logger.error('Error delivering command reply to B24', err);
710
+ },
711
+ },
712
+ });
713
+ } catch (err) {
714
+ logger.error('Error dispatching command to agent', err);
715
+ }
716
+ },
717
+
325
718
  onJoinChat: async (event: B24JoinChatEvent) => {
326
719
  logger.info('Bot joined chat', {
327
720
  dialogId: event.data.PARAMS.DIALOG_ID,
@@ -344,7 +737,7 @@ export const bitrix24Plugin = {
344
737
  },
345
738
  });
346
739
 
347
- gatewayState = { api, sendService, inboundHandler };
740
+ gatewayState = { api, sendService, mediaService, inboundHandler };
348
741
 
349
742
  logger.info(`[${ctx.accountId}] Bitrix24 channel started`);
350
743
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * OpenClaw bot commands to register with Bitrix24.
3
+ *
4
+ * Standard OpenClaw bot commands registered via imbot.command.register.
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
+ ];
@@ -6,9 +6,8 @@ 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
- dmPolicy: z.enum(['open', 'allowlist', 'pairing']).optional().default('open'),
10
+ dmPolicy: z.enum(['open', 'allowlist', 'pairing']).optional().default('pairing'),
12
11
  allowFrom: z.array(z.string()).optional(),
13
12
  showTyping: z.boolean().optional().default(true),
14
13
  streamUpdates: z.boolean().optional().default(false),
@@ -11,7 +11,6 @@ import type {
11
11
  B24MediaItem,
12
12
  } from './types.js';
13
13
  import { Dedup } from './dedup.js';
14
- import { checkAccess } from './access-control.js';
15
14
  import type { Bitrix24AccountConfig } from './types.js';
16
15
  import { defaultLogger } from './utils.js';
17
16
 
@@ -109,14 +108,6 @@ export class InboundHandler {
109
108
  return true;
110
109
  }
111
110
 
112
- const senderId = String(params.FROM_USER_ID);
113
-
114
- // Access control
115
- if (!checkAccess(senderId, this.config)) {
116
- this.logger.debug(`Access denied for user ${senderId}`);
117
- return true;
118
- }
119
-
120
111
  // Extract bot entry
121
112
  const botEntry = extractBotEntry(event.data.BOT);
122
113
  if (!botEntry) {
@@ -183,5 +174,6 @@ function normalizeFiles(files?: Record<string, B24File>): B24MediaItem[] {
183
174
  extension: file.extension,
184
175
  size: file.size,
185
176
  type: file.image ? 'image' as const : 'file' as const,
177
+ urlDownload: file.urlDownload || undefined,
186
178
  }));
187
179
  }