@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 +1 -1
- package/skills/bitrix24/SKILL.md +0 -25
- package/src/channel.ts +65 -6
- package/tests/channel.test.ts +2 -2
package/package.json
CHANGED
package/skills/bitrix24/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1507
|
-
|
|
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 () => {
|
package/tests/channel.test.ts
CHANGED
|
@@ -97,7 +97,7 @@ describe('convertButtonsToKeyboard', () => {
|
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
it('converts non-slash callback_data to ACTION
|
|
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('
|
|
109
|
+
expect(btn.ACTION_VALUE).toBe('Click me');
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|