@ihazz/bitrix24 1.0.1 → 1.0.2

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.2",
4
4
  "description": "Bitrix24 Messenger channel for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -63,31 +63,6 @@ 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
67
-
68
- You can attach inline buttons to a message. The `callback_data` value is sent back when the user taps the button.
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
- ```
83
-
84
- **Important rules for buttons:**
85
-
86
- - `callback_data` **must match** the button `text` exactly — when 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
-
91
66
  ## Writing Style (Bitrix24)
92
67
 
93
68
  - Direct, professional, moderate length.
package/src/channel.ts CHANGED
@@ -553,7 +553,9 @@ export function convertButtonsToKeyboard(rows: ChannelButton[][]): B24Keyboard {
553
553
  }
554
554
  } else if (btn.callback_data) {
555
555
  b24Btn.ACTION = 'SEND';
556
- b24Btn.ACTION_VALUE = btn.callback_data;
556
+ // Always use button text as ACTION_VALUE so the user sees readable text in chat,
557
+ // not opaque English identifiers like "answer_piano"
558
+ b24Btn.ACTION_VALUE = btn.text;
557
559
  }
558
560
 
559
561
  if (btn.style === 'primary') {
@@ -595,6 +597,39 @@ export function extractKeyboardFromPayload(
595
597
  return undefined;
596
598
  }
597
599
 
600
+ /**
601
+ * Extract inline button JSON embedded in message text by the agent.
602
+ *
603
+ * Agents (especially GPT-4o) sometimes embed button markup directly in text
604
+ * as `[[{...},{...}]]` instead of using tool call parameters.
605
+ * This function detects such patterns, extracts the keyboard, and returns
606
+ * cleaned text without the JSON fragment.
607
+ */
608
+ export function extractInlineButtonsFromText(
609
+ text: string,
610
+ ): { cleanText: string; keyboard: B24Keyboard } | undefined {
611
+ // Match [[...]] containing JSON array of button objects
612
+ const match = text.match(/\[\[\s*(\{[\s\S]*?\}(?:\s*,\s*\{[\s\S]*?\})*)\s*\]\]/);
613
+ if (!match) return undefined;
614
+
615
+ try {
616
+ const parsed = JSON.parse(`[${match[1]}]`);
617
+ if (!Array.isArray(parsed) || parsed.length === 0) return undefined;
618
+
619
+ // Validate it looks like button objects (must have "text" property)
620
+ if (!parsed.every((b: unknown) => typeof b === 'object' && b !== null && 'text' in b)) return undefined;
621
+
622
+ // Wrap in rows array (single row)
623
+ const buttons: ChannelButton[][] = [parsed as ChannelButton[]];
624
+ const keyboard = convertButtonsToKeyboard(buttons);
625
+ const cleanText = text.replace(match[0], '').trim();
626
+
627
+ return { cleanText, keyboard };
628
+ } catch {
629
+ return undefined;
630
+ }
631
+ }
632
+
598
633
  function normalizeCommandReplyPayload(params: {
599
634
  commandName: string;
600
635
  commandParams: string;
@@ -1066,7 +1101,17 @@ export const bitrix24Plugin = {
1066
1101
  }) => {
1067
1102
  const sendCtx = resolveOutboundSendCtx(ctx);
1068
1103
  if (!sendCtx || !gatewayState) throw new Error('Bitrix24 gateway not started');
1069
- const result = await gatewayState.sendService.sendText(sendCtx, ctx.text);
1104
+
1105
+ // Agent may embed button JSON in text as [[{...}]]
1106
+ let text = ctx.text;
1107
+ let keyboard: B24Keyboard | undefined;
1108
+ const extracted = extractInlineButtonsFromText(text);
1109
+ if (extracted) {
1110
+ text = extracted.cleanText;
1111
+ keyboard = extracted.keyboard;
1112
+ }
1113
+
1114
+ const result = await gatewayState.sendService.sendText(sendCtx, text, keyboard ? { keyboard } : undefined);
1070
1115
  return { messageId: String(result.messageId ?? '') };
1071
1116
  },
1072
1117
 
@@ -1177,8 +1222,11 @@ export const bitrix24Plugin = {
1177
1222
  if (!config.webhookUrl || !gatewayState) return null;
1178
1223
 
1179
1224
  const bot = gatewayState.bot;
1180
- const to = String(ctx.params.to ?? '').trim();
1181
- if (!to) return null;
1225
+ const to = String(ctx.params.to ?? ctx.to ?? '').trim();
1226
+ if (!to) {
1227
+ defaultLogger.warn('handleAction send: no "to" in params or ctx, falling back to gateway');
1228
+ return null;
1229
+ }
1182
1230
 
1183
1231
  const sendCtx: SendContext = { webhookUrl: config.webhookUrl, bot, dialogId: to };
1184
1232
  const message = String(ctx.params.message ?? '').trim();
@@ -1503,8 +1551,19 @@ export const bitrix24Plugin = {
1503
1551
  }
1504
1552
  }
1505
1553
  if (payload.text) {
1506
- const keyboard = extractKeyboardFromPayload(payload);
1507
- await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
1554
+ let text = payload.text;
1555
+ let keyboard = extractKeyboardFromPayload(payload);
1556
+
1557
+ // Fallback: agent may embed button JSON in text as [[{...}]]
1558
+ if (!keyboard) {
1559
+ const extracted = extractInlineButtonsFromText(text);
1560
+ if (extracted) {
1561
+ text = extracted.cleanText;
1562
+ keyboard = extracted.keyboard;
1563
+ }
1564
+ }
1565
+
1566
+ await sendService.sendText(sendCtx, text, keyboard ? { keyboard } : undefined);
1508
1567
  }
1509
1568
  },
1510
1569
  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