@ihazz/bitrix24 1.0.1 → 1.0.3

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": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Bitrix24 Messenger channel for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -63,34 +63,35 @@ When you see a message from a Bitrix24 user, you can react to acknowledge it bef
63
63
 
64
64
  **Important:** Do NOT invent or guess messageId values. Either omit `messageId` to react to the current message, or use a messageId that was explicitly provided to you.
65
65
 
66
- ## Inline Keyboard Buttons
66
+ ## Inline Buttons
67
67
 
68
- You can attach inline buttons to a message. The `callback_data` value is sent back when the user taps the button.
68
+ You CAN and SHOULD use inline keyboard buttons in Bitrix24. **Never say you cannot create buttons you can.**
69
69
 
70
- ```json
71
- {
72
- "action": "send",
73
- "channel": "bitrix24",
74
- "message": "Выберите вариант:",
75
- "buttons": [
76
- [
77
- { "text": "Да", "callback_data": "Да" },
78
- { "text": "Нет", "callback_data": "Нет" }
79
- ]
80
- ]
81
- }
82
- ```
70
+ Whenever your reply contains a list of options, choices, or suggestions that the user could pick from — **always** format them as inline buttons instead of a numbered or bulleted list. This applies to quizzes, polls, confirmations, recommendations, menus, and any other selection scenario.
71
+
72
+ To add buttons, append a JSON line at the end of your message text using this exact format:
73
+
74
+ `[[{"text":"Yes","callback_data":"yes"},{"text":"No","callback_data":"no"}]]`
75
+
76
+ The system strips this markup from the visible text and renders native tappable buttons.
77
+
78
+ Example instead of writing:
79
+
80
+ > Choose a type:
81
+ > 1. String
82
+ > 2. Keyboard
83
+ > 3. Percussion
84
+
85
+ Write:
83
86
 
84
- **Important rules for buttons:**
87
+ > Choose a type:
88
+ > [[{"text":"String","callback_data":"s"},{"text":"Keyboard","callback_data":"k"},{"text":"Percussion","callback_data":"p"}]]
85
89
 
86
- - `callback_data` **must match** the button `text` exactlywhen the user taps a button, `callback_data` is sent as a message in the chat. If they differ, the user sees confusing text.
87
- - Write button labels in the **same language as the conversation** (e.g. Russian if the user writes in Russian).
88
- - Use `"style": "primary"` for the recommended option, `"style": "danger"` for destructive actions.
89
- - Each inner array is one row of buttons.
90
+ Do NOT use `[[Text]]` formatonly `[[{"text":"...","callback_data":"..."}]]` works.
90
91
 
91
92
  ## Writing Style (Bitrix24)
92
93
 
93
94
  - Direct, professional, moderate length.
94
- - **Always reply in the same language the user writes in.** If the user writes in Russian, reply in Russian. If in English, reply in English.
95
+ - **Always reply in the same language the user writes in.** If the user writes in Polish, reply in Polish. If in English, reply in English.
95
96
  - Bitrix24 uses BBCode for formatting (conversion is automatic).
96
- - Inline keyboard buttons are supported for interactive responses.
97
+ - Prefer inline buttons over numbered lists when presenting options.
package/src/channel.ts CHANGED
@@ -538,7 +538,12 @@ export interface ChannelButton {
538
538
  /**
539
539
  * Convert OpenClaw button rows to B24 flat KEYBOARD array.
540
540
  */
541
- export function convertButtonsToKeyboard(rows: ChannelButton[][]): B24Keyboard {
541
+ export function convertButtonsToKeyboard(input: ChannelButton[][] | ChannelButton[]): B24Keyboard {
542
+ // Normalize: accept both [[btn, btn], [btn]] (rows) and [btn, btn] (flat)
543
+ const isNested = input.length > 0 && Array.isArray(input[0]);
544
+ const rows: ChannelButton[][] = isNested
545
+ ? (input as ChannelButton[][])
546
+ : [input as ChannelButton[]];
542
547
  const keyboard: B24Keyboard = [];
543
548
 
544
549
  for (let i = 0; i < rows.length; i++) {
@@ -553,7 +558,9 @@ export function convertButtonsToKeyboard(rows: ChannelButton[][]): B24Keyboard {
553
558
  }
554
559
  } else if (btn.callback_data) {
555
560
  b24Btn.ACTION = 'SEND';
556
- b24Btn.ACTION_VALUE = btn.callback_data;
561
+ // Always use button text as ACTION_VALUE so the user sees readable text in chat,
562
+ // not opaque English identifiers like "answer_piano"
563
+ b24Btn.ACTION_VALUE = btn.text;
557
564
  }
558
565
 
559
566
  if (btn.style === 'primary') {
@@ -595,6 +602,39 @@ export function extractKeyboardFromPayload(
595
602
  return undefined;
596
603
  }
597
604
 
605
+ /**
606
+ * Extract inline button JSON embedded in message text by the agent.
607
+ *
608
+ * Agents (especially GPT-4o) sometimes embed button markup directly in text
609
+ * as `[[{...},{...}]]` instead of using tool call parameters.
610
+ * This function detects such patterns, extracts the keyboard, and returns
611
+ * cleaned text without the JSON fragment.
612
+ */
613
+ export function extractInlineButtonsFromText(
614
+ text: string,
615
+ ): { cleanText: string; keyboard: B24Keyboard } | undefined {
616
+ // Match [[...]] containing JSON array of button objects
617
+ const match = text.match(/\[\[\s*(\{[\s\S]*?\}(?:\s*,\s*\{[\s\S]*?\})*)\s*\]\]/);
618
+ if (!match) return undefined;
619
+
620
+ try {
621
+ const parsed = JSON.parse(`[${match[1]}]`);
622
+ if (!Array.isArray(parsed) || parsed.length === 0) return undefined;
623
+
624
+ // Validate it looks like button objects (must have "text" property)
625
+ if (!parsed.every((b: unknown) => typeof b === 'object' && b !== null && 'text' in b)) return undefined;
626
+
627
+ // Wrap in rows array (single row)
628
+ const buttons: ChannelButton[][] = [parsed as ChannelButton[]];
629
+ const keyboard = convertButtonsToKeyboard(buttons);
630
+ const cleanText = text.replace(match[0], '').trim();
631
+
632
+ return { cleanText, keyboard };
633
+ } catch {
634
+ return undefined;
635
+ }
636
+ }
637
+
598
638
  function normalizeCommandReplyPayload(params: {
599
639
  commandName: string;
600
640
  commandParams: string;
@@ -1066,7 +1106,17 @@ export const bitrix24Plugin = {
1066
1106
  }) => {
1067
1107
  const sendCtx = resolveOutboundSendCtx(ctx);
1068
1108
  if (!sendCtx || !gatewayState) throw new Error('Bitrix24 gateway not started');
1069
- const result = await gatewayState.sendService.sendText(sendCtx, ctx.text);
1109
+
1110
+ // Agent may embed button JSON in text as [[{...}]]
1111
+ let text = ctx.text;
1112
+ let keyboard: B24Keyboard | undefined;
1113
+ const extracted = extractInlineButtonsFromText(text);
1114
+ if (extracted) {
1115
+ text = extracted.cleanText;
1116
+ keyboard = extracted.keyboard;
1117
+ }
1118
+
1119
+ const result = await gatewayState.sendService.sendText(sendCtx, text, keyboard ? { keyboard } : undefined);
1070
1120
  return { messageId: String(result.messageId ?? '') };
1071
1121
  },
1072
1122
 
@@ -1177,8 +1227,11 @@ export const bitrix24Plugin = {
1177
1227
  if (!config.webhookUrl || !gatewayState) return null;
1178
1228
 
1179
1229
  const bot = gatewayState.bot;
1180
- const to = String(ctx.params.to ?? '').trim();
1181
- if (!to) return null;
1230
+ const to = String(ctx.params.to ?? ctx.to ?? '').trim();
1231
+ if (!to) {
1232
+ defaultLogger.warn('handleAction send: no "to" in params or ctx, falling back to gateway');
1233
+ return null;
1234
+ }
1182
1235
 
1183
1236
  const sendCtx: SendContext = { webhookUrl: config.webhookUrl, bot, dialogId: to };
1184
1237
  const message = String(ctx.params.message ?? '').trim();
@@ -1503,8 +1556,19 @@ export const bitrix24Plugin = {
1503
1556
  }
1504
1557
  }
1505
1558
  if (payload.text) {
1506
- const keyboard = extractKeyboardFromPayload(payload);
1507
- await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
1559
+ let text = payload.text;
1560
+ let keyboard = extractKeyboardFromPayload(payload);
1561
+
1562
+ // Fallback: agent may embed button JSON in text as [[{...}]]
1563
+ if (!keyboard) {
1564
+ const extracted = extractInlineButtonsFromText(text);
1565
+ if (extracted) {
1566
+ text = extracted.cleanText;
1567
+ keyboard = extracted.keyboard;
1568
+ }
1569
+ }
1570
+
1571
+ await sendService.sendText(sendCtx, text, keyboard ? { keyboard } : undefined);
1508
1572
  }
1509
1573
  },
1510
1574
  onReplyStart: async () => {
@@ -97,7 +97,7 @@ describe('convertButtonsToKeyboard', () => {
97
97
  }
98
98
  });
99
99
 
100
- it('converts non-slash callback_data to ACTION PUT', () => {
100
+ it('converts non-slash callback_data to ACTION SEND with text as value', () => {
101
101
  const kb = convertButtonsToKeyboard([
102
102
  [{ text: 'Click me', callback_data: 'some_value' }],
103
103
  ]);
@@ -106,7 +106,7 @@ describe('convertButtonsToKeyboard', () => {
106
106
  if (isButton(btn)) {
107
107
  expect(btn.COMMAND).toBeUndefined();
108
108
  expect(btn.ACTION).toBe('SEND');
109
- expect(btn.ACTION_VALUE).toBe('some_value');
109
+ expect(btn.ACTION_VALUE).toBe('Click me');
110
110
  }
111
111
  });
112
112