@ihazz/bitrix24 1.0.0 → 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/README.md +33 -5
- package/package.json +1 -1
- package/skills/bitrix24/SKILL.md +1 -0
- package/src/channel.ts +116 -7
- package/tests/channel.test.ts +3 -3
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Create an inbound webhook in Bitrix24:
|
|
|
37
37
|
2. Create a webhook for your OpenClaw bot
|
|
38
38
|
3. Grant scopes:
|
|
39
39
|
- `imbot` — the minimum scope required for full bot operation
|
|
40
|
-
- `im` —
|
|
40
|
+
- `im` — reserved for future `agentMode` support; `agentMode` is not production-ready yet and will be available in future versions
|
|
41
41
|
- you may also grant any extra scopes beyond this set if they are needed for your Bitrix24 scenarios
|
|
42
42
|
4. Save the webhook and copy the URL
|
|
43
43
|
|
|
@@ -94,15 +94,17 @@ If `eventMode` is omitted, the plugin uses:
|
|
|
94
94
|
| `eventMode` | auto | `fetch` or `webhook`. Auto-selects from `callbackUrl` when omitted. |
|
|
95
95
|
| `botName` | `"OpenClaw"` | Bot display name. |
|
|
96
96
|
| `botCode` | auto | Optional explicit bot code. If omitted, the plugin registers the bot as `openclaw_<webhookUserId>`, and if that code is occupied it tries `openclaw_<webhookUserId>_2`, `_3`, and so on. |
|
|
97
|
-
| `botToken` |
|
|
97
|
+
| `botToken` | `md5(webhookUrl)` | Bot token for `imbot.v2` authentication (32-char hex string). If omitted, derived as `md5(webhookUrl)`. **Strongly recommended to set explicitly** if your `webhookUrl` may change (e.g. ngrok, dynamic DNS) — otherwise the plugin loses control of the previously registered bot. You can find the token in the Bitrix24 admin panel or compute it as `md5` of the original webhook URL. |
|
|
98
98
|
| `botAvatar` | — | Optional base64 avatar override. |
|
|
99
99
|
| `dmPolicy` | `"webhookUser"` | Access policy: `webhookUser` or `pairing`. |
|
|
100
100
|
| `showTyping` | `true` | Sends typing indicator before response. |
|
|
101
101
|
| `enabled` | `true` | Enables or disables the account. |
|
|
102
|
-
| `agentMode` | `false` |
|
|
102
|
+
| `agentMode` | `false` | Reserved for future user-event integration. Not working yet; planned for future versions. |
|
|
103
103
|
| `pollingIntervalMs` | `3000` | Base poll interval for `fetch` mode. |
|
|
104
104
|
| `pollingFastIntervalMs` | `100` | Fast follow-up poll interval when more events are pending. |
|
|
105
105
|
|
|
106
|
+
### Agent-Driven Reactions
|
|
107
|
+
|
|
106
108
|
To enable agent-driven reactions, add `reactions` to the channel capabilities in your OpenClaw config:
|
|
107
109
|
|
|
108
110
|
```json
|
|
@@ -118,6 +120,32 @@ To enable agent-driven reactions, add `reactions` to the channel capabilities in
|
|
|
118
120
|
|
|
119
121
|
The plugin maps standard Unicode emoji (👍, 🔥, 👀, etc.) to Bitrix24 reaction codes automatically. B24 reaction codes (like `like`, `fire`, `eyes`) can also be used directly.
|
|
120
122
|
|
|
123
|
+
### Inline Buttons
|
|
124
|
+
|
|
125
|
+
To enable agent-driven inline buttons, add `inlineButtons` to the channel capabilities in your OpenClaw config:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"channels": {
|
|
130
|
+
"bitrix24": {
|
|
131
|
+
"webhookUrl": "...",
|
|
132
|
+
"capabilities": ["inlineButtons"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The plugin supports two button input forms:
|
|
139
|
+
|
|
140
|
+
- native Bitrix24 keyboard payload via `channelData.bitrix24.keyboard`
|
|
141
|
+
- generic OpenClaw button rows via `channelData.telegram.buttons`, which the plugin converts to Bitrix24 `KEYBOARD`
|
|
142
|
+
|
|
143
|
+
For converted generic buttons:
|
|
144
|
+
|
|
145
|
+
- `callback_data` starting with `/` becomes a native Bitrix24 slash command button
|
|
146
|
+
- any other `callback_data` becomes `ACTION=PUT`
|
|
147
|
+
- `primary`, `attention`, and `danger` button styles are mapped to Bitrix24 color tokens
|
|
148
|
+
|
|
121
149
|
## Access Policies
|
|
122
150
|
|
|
123
151
|
### `webhookUser`
|
|
@@ -161,7 +189,7 @@ openclaw pairing approve bitrix24 <CODE>
|
|
|
161
189
|
- Direct messages
|
|
162
190
|
- Typing indicators
|
|
163
191
|
- Text replies with BBCode conversion
|
|
164
|
-
- Inline keyboard buttons
|
|
192
|
+
- Inline keyboard buttons (`channelData.bitrix24.keyboard` and converted generic button rows)
|
|
165
193
|
- Reactions (agent can add/remove reactions on messages via `imbot.v2.Chat.Message.Reaction.*`)
|
|
166
194
|
- File upload to chat via `imbot.v2.File.upload`
|
|
167
195
|
- File download via `imbot.v2.File.download`
|
|
@@ -188,7 +216,7 @@ There is already account-config scaffolding in the plugin, but the current produ
|
|
|
188
216
|
|
|
189
217
|
- Verify `webhookUrl` is valid and active.
|
|
190
218
|
- If you use `eventMode: "webhook"`, verify `callbackUrl` is reachable from the internet over HTTPS.
|
|
191
|
-
-
|
|
219
|
+
- Do not rely on `agentMode` yet. It is not working in the current release and is planned for future versions.
|
|
192
220
|
- Do not expect the bot to work in group chats.
|
|
193
221
|
- If file transfer fails, verify the file size and Bitrix24-side availability of the attachment.
|
|
194
222
|
|
package/package.json
CHANGED
package/skills/bitrix24/SKILL.md
CHANGED
|
@@ -66,5 +66,6 @@ When you see a message from a Bitrix24 user, you can react to acknowledge it bef
|
|
|
66
66
|
## Writing Style (Bitrix24)
|
|
67
67
|
|
|
68
68
|
- Direct, professional, moderate length.
|
|
69
|
+
- **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.
|
|
69
70
|
- Bitrix24 uses BBCode for formatting (conversion is automatic).
|
|
70
71
|
- Inline keyboard buttons are supported for interactive responses.
|
package/src/channel.ts
CHANGED
|
@@ -552,8 +552,10 @@ export function convertButtonsToKeyboard(rows: ChannelButton[][]): B24Keyboard {
|
|
|
552
552
|
b24Btn.COMMAND_PARAMS = parts.slice(1).join(' ');
|
|
553
553
|
}
|
|
554
554
|
} else if (btn.callback_data) {
|
|
555
|
-
b24Btn.ACTION = '
|
|
556
|
-
|
|
555
|
+
b24Btn.ACTION = 'SEND';
|
|
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
|
|
|
@@ -1152,11 +1197,11 @@ export const bitrix24Plugin = {
|
|
|
1152
1197
|
|
|
1153
1198
|
actions: {
|
|
1154
1199
|
listActions: (_params: { cfg: Record<string, unknown> }): string[] => {
|
|
1155
|
-
return ['react'];
|
|
1200
|
+
return ['react', 'send'];
|
|
1156
1201
|
},
|
|
1157
1202
|
|
|
1158
1203
|
supportsAction: (params: { action: string }): boolean => {
|
|
1159
|
-
return params.action === 'react';
|
|
1204
|
+
return params.action === 'react' || params.action === 'send';
|
|
1160
1205
|
},
|
|
1161
1206
|
|
|
1162
1207
|
handleAction: async (ctx: {
|
|
@@ -1167,6 +1212,59 @@ export const bitrix24Plugin = {
|
|
|
1167
1212
|
params: Record<string, unknown>;
|
|
1168
1213
|
[key: string]: unknown;
|
|
1169
1214
|
}): Promise<Record<string, unknown> | null> => {
|
|
1215
|
+
// ─── Send with buttons ──────────────────────────────────────────────
|
|
1216
|
+
if (ctx.action === 'send') {
|
|
1217
|
+
// Only intercept send when buttons are present; otherwise let gateway handle normally
|
|
1218
|
+
const rawButtons = ctx.params.buttons;
|
|
1219
|
+
if (!rawButtons) return null;
|
|
1220
|
+
|
|
1221
|
+
const { config } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
1222
|
+
if (!config.webhookUrl || !gatewayState) return null;
|
|
1223
|
+
|
|
1224
|
+
const bot = gatewayState.bot;
|
|
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
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const sendCtx: SendContext = { webhookUrl: config.webhookUrl, bot, dialogId: to };
|
|
1232
|
+
const message = String(ctx.params.message ?? '').trim();
|
|
1233
|
+
|
|
1234
|
+
// Parse buttons: may be array or JSON string
|
|
1235
|
+
let buttons: ChannelButton[][] | undefined;
|
|
1236
|
+
try {
|
|
1237
|
+
const parsed = typeof rawButtons === 'string' ? JSON.parse(rawButtons) : rawButtons;
|
|
1238
|
+
if (Array.isArray(parsed)) buttons = parsed;
|
|
1239
|
+
} catch {
|
|
1240
|
+
// invalid buttons JSON — send without keyboard
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const keyboard = buttons?.length ? convertButtonsToKeyboard(buttons) : undefined;
|
|
1244
|
+
|
|
1245
|
+
const toolResult = (payload: Record<string, unknown>) => ({
|
|
1246
|
+
content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }],
|
|
1247
|
+
details: payload,
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
try {
|
|
1251
|
+
const result = await gatewayState.sendService.sendText(
|
|
1252
|
+
sendCtx, message || ' ', keyboard ? { keyboard } : undefined,
|
|
1253
|
+
);
|
|
1254
|
+
return toolResult({
|
|
1255
|
+
channel: 'bitrix24',
|
|
1256
|
+
to,
|
|
1257
|
+
via: 'direct',
|
|
1258
|
+
mediaUrl: null,
|
|
1259
|
+
result: { messageId: String(result.messageId ?? '') },
|
|
1260
|
+
});
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1263
|
+
return toolResult({ ok: false, error: errMsg });
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// ─── React ────────────────────────────────────────────────────────────
|
|
1170
1268
|
if (ctx.action !== 'react') return null;
|
|
1171
1269
|
|
|
1172
1270
|
// Helper: wrap payload as gateway-compatible tool result
|
|
@@ -1453,8 +1551,19 @@ export const bitrix24Plugin = {
|
|
|
1453
1551
|
}
|
|
1454
1552
|
}
|
|
1455
1553
|
if (payload.text) {
|
|
1456
|
-
|
|
1457
|
-
|
|
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);
|
|
1458
1567
|
}
|
|
1459
1568
|
},
|
|
1460
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
|
]);
|
|
@@ -105,8 +105,8 @@ describe('convertButtonsToKeyboard', () => {
|
|
|
105
105
|
const btn = kb[0];
|
|
106
106
|
if (isButton(btn)) {
|
|
107
107
|
expect(btn.COMMAND).toBeUndefined();
|
|
108
|
-
expect(btn.ACTION).toBe('
|
|
109
|
-
expect(btn.ACTION_VALUE).toBe('
|
|
108
|
+
expect(btn.ACTION).toBe('SEND');
|
|
109
|
+
expect(btn.ACTION_VALUE).toBe('Click me');
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|