@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 +1 -1
- package/skills/bitrix24/SKILL.md +23 -22
- package/src/channel.ts +71 -7
- package/tests/channel.test.ts +2 -2
package/package.json
CHANGED
package/skills/bitrix24/SKILL.md
CHANGED
|
@@ -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
|
|
66
|
+
## Inline Buttons
|
|
67
67
|
|
|
68
|
-
You
|
|
68
|
+
You CAN and SHOULD use inline keyboard buttons in Bitrix24. **Never say you cannot create buttons — you can.**
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
87
|
+
> Choose a type:
|
|
88
|
+
> [[{"text":"String","callback_data":"s"},{"text":"Keyboard","callback_data":"k"},{"text":"Percussion","callback_data":"p"}]]
|
|
85
89
|
|
|
86
|
-
|
|
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]]` format — only `[[{"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
|
|
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
|
-
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1507
|
-
|
|
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 () => {
|
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
|
|