@ihazz/bitrix24 1.1.2 → 1.1.4

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.
Files changed (104) hide show
  1. package/README.md +5 -0
  2. package/dist/index.d.ts +30 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +55 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/access-control.d.ts +43 -0
  7. package/dist/src/access-control.d.ts.map +1 -0
  8. package/dist/src/access-control.js +128 -0
  9. package/dist/src/access-control.js.map +1 -0
  10. package/dist/src/api.d.ts +161 -0
  11. package/dist/src/api.d.ts.map +1 -0
  12. package/dist/src/api.js +357 -0
  13. package/dist/src/api.js.map +1 -0
  14. package/dist/src/bot-avatar.d.ts +7 -0
  15. package/dist/src/bot-avatar.d.ts.map +1 -0
  16. package/dist/src/bot-avatar.js +7 -0
  17. package/dist/src/bot-avatar.js.map +1 -0
  18. package/dist/src/channel.d.ts +216 -0
  19. package/dist/src/channel.d.ts.map +1 -0
  20. package/dist/src/channel.js +2324 -0
  21. package/dist/src/channel.js.map +1 -0
  22. package/dist/src/commands.d.ts +22 -0
  23. package/dist/src/commands.d.ts.map +1 -0
  24. package/dist/src/commands.js +160 -0
  25. package/dist/src/commands.js.map +1 -0
  26. package/dist/src/config-schema.d.ts +356 -0
  27. package/dist/src/config-schema.d.ts.map +1 -0
  28. package/dist/src/config-schema.js +43 -0
  29. package/dist/src/config-schema.js.map +1 -0
  30. package/dist/src/config.d.ts +11 -0
  31. package/dist/src/config.d.ts.map +1 -0
  32. package/dist/src/config.js +50 -0
  33. package/dist/src/config.js.map +1 -0
  34. package/dist/src/dedup.d.ts +22 -0
  35. package/dist/src/dedup.d.ts.map +1 -0
  36. package/dist/src/dedup.js +49 -0
  37. package/dist/src/dedup.js.map +1 -0
  38. package/dist/src/group-access.d.ts +52 -0
  39. package/dist/src/group-access.d.ts.map +1 -0
  40. package/dist/src/group-access.js +180 -0
  41. package/dist/src/group-access.js.map +1 -0
  42. package/dist/src/history-cache.d.ts +41 -0
  43. package/dist/src/history-cache.d.ts.map +1 -0
  44. package/dist/src/history-cache.js +82 -0
  45. package/dist/src/history-cache.js.map +1 -0
  46. package/dist/src/i18n.d.ts +22 -0
  47. package/dist/src/i18n.d.ts.map +1 -0
  48. package/dist/src/i18n.js +175 -0
  49. package/dist/src/i18n.js.map +1 -0
  50. package/dist/src/inbound-handler.d.ts +92 -0
  51. package/dist/src/inbound-handler.d.ts.map +1 -0
  52. package/dist/src/inbound-handler.js +417 -0
  53. package/dist/src/inbound-handler.js.map +1 -0
  54. package/dist/src/media-service.d.ts +52 -0
  55. package/dist/src/media-service.d.ts.map +1 -0
  56. package/dist/src/media-service.js +423 -0
  57. package/dist/src/media-service.js.map +1 -0
  58. package/dist/src/message-utils.d.ts +34 -0
  59. package/dist/src/message-utils.d.ts.map +1 -0
  60. package/dist/src/message-utils.js +392 -0
  61. package/dist/src/message-utils.js.map +1 -0
  62. package/dist/src/polling-service.d.ts +39 -0
  63. package/dist/src/polling-service.d.ts.map +1 -0
  64. package/dist/src/polling-service.js +204 -0
  65. package/dist/src/polling-service.js.map +1 -0
  66. package/dist/src/rate-limiter.d.ts +22 -0
  67. package/dist/src/rate-limiter.d.ts.map +1 -0
  68. package/dist/src/rate-limiter.js +72 -0
  69. package/dist/src/rate-limiter.js.map +1 -0
  70. package/dist/src/runtime.d.ts +106 -0
  71. package/dist/src/runtime.d.ts.map +1 -0
  72. package/dist/src/runtime.js +11 -0
  73. package/dist/src/runtime.js.map +1 -0
  74. package/dist/src/send-service.d.ts +66 -0
  75. package/dist/src/send-service.d.ts.map +1 -0
  76. package/dist/src/send-service.js +177 -0
  77. package/dist/src/send-service.js.map +1 -0
  78. package/dist/src/state-paths.d.ts +3 -0
  79. package/dist/src/state-paths.d.ts.map +1 -0
  80. package/dist/src/state-paths.js +23 -0
  81. package/dist/src/state-paths.js.map +1 -0
  82. package/dist/src/types.d.ts +381 -0
  83. package/dist/src/types.d.ts.map +1 -0
  84. package/dist/src/types.js +3 -0
  85. package/dist/src/types.js.map +1 -0
  86. package/dist/src/utils.d.ts +60 -0
  87. package/dist/src/utils.d.ts.map +1 -0
  88. package/dist/src/utils.js +131 -0
  89. package/dist/src/utils.js.map +1 -0
  90. package/index.ts +1 -1
  91. package/openclaw.plugin.json +278 -1
  92. package/package.json +11 -2
  93. package/src/api.ts +0 -3
  94. package/src/channel.ts +76 -73
  95. package/src/config-schema.ts +1 -2
  96. package/src/config.ts +6 -8
  97. package/src/group-access.ts +1 -8
  98. package/src/inbound-handler.ts +128 -15
  99. package/src/media-service.ts +160 -45
  100. package/src/polling-service.ts +2 -3
  101. package/src/send-service.ts +4 -3
  102. package/src/state-paths.ts +4 -0
  103. package/src/types.ts +1 -2
  104. package/src/utils.ts +31 -4
@@ -5,6 +5,283 @@
5
5
  "configSchema": {
6
6
  "type": "object",
7
7
  "additionalProperties": true,
8
- "properties": {}
8
+ "$defs": {
9
+ "watchRule": {
10
+ "type": "object",
11
+ "additionalProperties": true,
12
+ "properties": {
13
+ "userId": {
14
+ "type": "string",
15
+ "minLength": 1
16
+ },
17
+ "topics": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "string",
21
+ "minLength": 1
22
+ }
23
+ },
24
+ "mode": {
25
+ "type": "string",
26
+ "enum": ["reply", "notifyOwnerDm"]
27
+ }
28
+ }
29
+ },
30
+ "agentWatchRule": {
31
+ "type": "object",
32
+ "additionalProperties": true,
33
+ "properties": {
34
+ "userId": {
35
+ "type": "string",
36
+ "minLength": 1
37
+ },
38
+ "topics": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ }
44
+ },
45
+ "mode": {
46
+ "type": "string",
47
+ "enum": ["notifyOwnerDm"]
48
+ }
49
+ }
50
+ },
51
+ "groupConfig": {
52
+ "type": "object",
53
+ "additionalProperties": true,
54
+ "properties": {
55
+ "groupPolicy": {
56
+ "type": "string",
57
+ "enum": ["disabled", "webhookUser", "pairing", "allowlist", "open"]
58
+ },
59
+ "requireMention": {
60
+ "type": "boolean",
61
+ "default": true
62
+ },
63
+ "allowFrom": {
64
+ "type": "array",
65
+ "items": {
66
+ "type": "string"
67
+ }
68
+ },
69
+ "watch": {
70
+ "type": "array",
71
+ "items": {
72
+ "$ref": "#/$defs/watchRule"
73
+ }
74
+ }
75
+ }
76
+ },
77
+ "accountConfig": {
78
+ "type": "object",
79
+ "additionalProperties": true,
80
+ "properties": {
81
+ "enabled": {
82
+ "type": "boolean",
83
+ "default": true
84
+ },
85
+ "webhookUrl": {
86
+ "type": "string",
87
+ "pattern": "^https?://\\S+$"
88
+ },
89
+ "botToken": {
90
+ "type": "string"
91
+ },
92
+ "botName": {
93
+ "type": "string",
94
+ "default": "OpenClaw"
95
+ },
96
+ "botCode": {
97
+ "type": "string"
98
+ },
99
+ "botAvatar": {
100
+ "type": "string"
101
+ },
102
+ "allowFrom": {
103
+ "type": "array",
104
+ "items": {
105
+ "type": "string"
106
+ }
107
+ },
108
+ "callbackUrl": {
109
+ "type": "string",
110
+ "pattern": "^https?://\\S+$"
111
+ },
112
+ "eventMode": {
113
+ "type": "string",
114
+ "enum": ["fetch", "webhook"]
115
+ },
116
+ "agentMode": {
117
+ "type": "boolean",
118
+ "default": false
119
+ },
120
+ "pollingIntervalMs": {
121
+ "type": "integer",
122
+ "minimum": 500,
123
+ "default": 3000
124
+ },
125
+ "pollingFastIntervalMs": {
126
+ "type": "integer",
127
+ "minimum": 50,
128
+ "default": 100
129
+ },
130
+ "dmPolicy": {
131
+ "type": "string",
132
+ "enum": ["pairing", "webhookUser", "allowlist", "open"],
133
+ "default": "webhookUser"
134
+ },
135
+ "groupPolicy": {
136
+ "type": "string",
137
+ "enum": ["disabled", "webhookUser", "pairing", "allowlist", "open"],
138
+ "default": "webhookUser"
139
+ },
140
+ "groupAllowFrom": {
141
+ "type": "array",
142
+ "items": {
143
+ "type": "string"
144
+ }
145
+ },
146
+ "requireMention": {
147
+ "type": "boolean",
148
+ "default": true
149
+ },
150
+ "historyLimit": {
151
+ "type": "integer",
152
+ "minimum": 0,
153
+ "default": 100
154
+ },
155
+ "groups": {
156
+ "type": "object",
157
+ "additionalProperties": {
158
+ "$ref": "#/$defs/groupConfig"
159
+ }
160
+ },
161
+ "agentWatch": {
162
+ "type": "object",
163
+ "additionalProperties": {
164
+ "type": "array",
165
+ "items": {
166
+ "$ref": "#/$defs/agentWatchRule"
167
+ }
168
+ }
169
+ },
170
+ "showTyping": {
171
+ "type": "boolean",
172
+ "default": true
173
+ },
174
+ "verboseLog": {
175
+ "type": "boolean",
176
+ "default": false
177
+ }
178
+ }
179
+ }
180
+ },
181
+ "properties": {
182
+ "enabled": {
183
+ "type": "boolean",
184
+ "default": true
185
+ },
186
+ "webhookUrl": {
187
+ "type": "string",
188
+ "pattern": "^https?://\\S+$"
189
+ },
190
+ "botToken": {
191
+ "type": "string"
192
+ },
193
+ "botName": {
194
+ "type": "string",
195
+ "default": "OpenClaw"
196
+ },
197
+ "botCode": {
198
+ "type": "string"
199
+ },
200
+ "botAvatar": {
201
+ "type": "string"
202
+ },
203
+ "allowFrom": {
204
+ "type": "array",
205
+ "items": {
206
+ "type": "string"
207
+ }
208
+ },
209
+ "callbackUrl": {
210
+ "type": "string",
211
+ "pattern": "^https?://\\S+$"
212
+ },
213
+ "eventMode": {
214
+ "type": "string",
215
+ "enum": ["fetch", "webhook"]
216
+ },
217
+ "agentMode": {
218
+ "type": "boolean",
219
+ "default": false
220
+ },
221
+ "pollingIntervalMs": {
222
+ "type": "integer",
223
+ "minimum": 500,
224
+ "default": 3000
225
+ },
226
+ "pollingFastIntervalMs": {
227
+ "type": "integer",
228
+ "minimum": 50,
229
+ "default": 100
230
+ },
231
+ "dmPolicy": {
232
+ "type": "string",
233
+ "enum": ["pairing", "webhookUser", "allowlist", "open"],
234
+ "default": "webhookUser"
235
+ },
236
+ "groupPolicy": {
237
+ "type": "string",
238
+ "enum": ["disabled", "webhookUser", "pairing", "allowlist", "open"],
239
+ "default": "webhookUser"
240
+ },
241
+ "groupAllowFrom": {
242
+ "type": "array",
243
+ "items": {
244
+ "type": "string"
245
+ }
246
+ },
247
+ "requireMention": {
248
+ "type": "boolean",
249
+ "default": true
250
+ },
251
+ "historyLimit": {
252
+ "type": "integer",
253
+ "minimum": 0,
254
+ "default": 100
255
+ },
256
+ "groups": {
257
+ "type": "object",
258
+ "additionalProperties": {
259
+ "$ref": "#/$defs/groupConfig"
260
+ }
261
+ },
262
+ "agentWatch": {
263
+ "type": "object",
264
+ "additionalProperties": {
265
+ "type": "array",
266
+ "items": {
267
+ "$ref": "#/$defs/agentWatchRule"
268
+ }
269
+ }
270
+ },
271
+ "showTyping": {
272
+ "type": "boolean",
273
+ "default": true
274
+ },
275
+ "verboseLog": {
276
+ "type": "boolean",
277
+ "default": false
278
+ },
279
+ "accounts": {
280
+ "type": "object",
281
+ "additionalProperties": {
282
+ "$ref": "#/$defs/accountConfig"
283
+ }
284
+ }
285
+ }
9
286
  }
10
287
  }
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "@ihazz/bitrix24",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Bitrix24 Messenger channel for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
8
15
  "files": [
16
+ "dist",
9
17
  "index.ts",
10
18
  "openclaw.plugin.json",
11
19
  "README.md",
@@ -15,7 +23,7 @@
15
23
  ],
16
24
  "openclaw": {
17
25
  "extensions": [
18
- "./index.ts"
26
+ "./dist/index.js"
19
27
  ],
20
28
  "channel": {
21
29
  "id": "bitrix24",
@@ -33,6 +41,7 @@
33
41
  },
34
42
  "scripts": {
35
43
  "build": "tsc",
44
+ "prepack": "npm run build",
36
45
  "test": "vitest run",
37
46
  "test:watch": "vitest"
38
47
  },
package/src/api.ts CHANGED
@@ -439,9 +439,6 @@ export class Bitrix24Api {
439
439
  return result.result;
440
440
  }
441
441
 
442
- /**
443
- * Backward-compatible wrapper for typing indicator.
444
- */
445
442
  /**
446
443
  * Mark messages as read (imbot.v2.Chat.Message.read).
447
444
  */
package/src/channel.ts CHANGED
@@ -9,7 +9,7 @@ import type { CommandSendContext, SendContext } from './send-service.js';
9
9
  import { MediaService } from './media-service.js';
10
10
  import type { DownloadedMedia } from './media-service.js';
11
11
  import { InboundHandler } from './inbound-handler.js';
12
- import type { FetchCommandContext, FetchJoinChatContext } from './inbound-handler.js';
12
+ import type { FetchCommandContext, FetchJoinChatContext, FetchReactionContext } from './inbound-handler.js';
13
13
  import { PollingService } from './polling-service.js';
14
14
  import {
15
15
  normalizeAllowEntry,
@@ -24,7 +24,7 @@ import {
24
24
  resolveGroupAccess,
25
25
  } from './group-access.js';
26
26
  import { DEFAULT_AVATAR_BASE64 } from './bot-avatar.js';
27
- import { Bitrix24ApiError, defaultLogger, CHANNEL_PREFIX_RE } from './utils.js';
27
+ import { Bitrix24ApiError, createVerboseLogger, defaultLogger, CHANNEL_PREFIX_RE } from './utils.js';
28
28
  import { getBitrix24Runtime } from './runtime.js';
29
29
  import type { ChannelPairingAdapter } from './runtime.js';
30
30
  import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply } from './commands.js';
@@ -75,6 +75,7 @@ const HISTORY_CACHE_MAX_KEYS = 1000;
75
75
  const HISTORY_CONTEXT_MARKER = '[Chat messages since your last reply - for context]';
76
76
  const CROSS_CHAT_HISTORY_LIMIT = 20;
77
77
  const ACCESS_DENIED_REACTION = 'crossMark';
78
+ const BOT_MESSAGE_WATCH_REACTION = 'eyes';
78
79
  const FORWARDED_CONTEXT_RANGE = 5;
79
80
  const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
80
81
 
@@ -1407,19 +1408,16 @@ function collectOutboundMediaUrls(input: {
1407
1408
  }
1408
1409
 
1409
1410
  async function uploadOutboundMedia(params: {
1410
- sendCtx: SendContext;
1411
+ mediaService: MediaService;
1411
1412
  mediaUrls: string[];
1412
- text?: string;
1413
+ sendCtx: SendContext;
1414
+ initialMessage?: string;
1413
1415
  }): Promise<string> {
1414
- if (!gatewayState) {
1415
- throw new Error('Bitrix24 gateway not started');
1416
- }
1417
-
1418
1416
  let lastMessageId = '';
1419
- let message = params.text;
1417
+ let message = params.initialMessage;
1420
1418
 
1421
1419
  for (const mediaUrl of params.mediaUrls) {
1422
- const result = await gatewayState.mediaService.uploadMediaToChat({
1420
+ const result = await params.mediaService.uploadMediaToChat({
1423
1421
  localPath: mediaUrl,
1424
1422
  fileName: basename(mediaUrl),
1425
1423
  webhookUrl: params.sendCtx.webhookUrl,
@@ -1562,9 +1560,10 @@ export const bitrix24Plugin = {
1562
1560
  const mediaUrls = collectOutboundMediaUrls({ mediaUrl: ctx.mediaUrl });
1563
1561
  if (mediaUrls.length > 0) {
1564
1562
  const messageId = await uploadOutboundMedia({
1563
+ mediaService: gatewayState.mediaService,
1565
1564
  sendCtx,
1566
1565
  mediaUrls,
1567
- text: ctx.text,
1566
+ initialMessage: ctx.text,
1568
1567
  });
1569
1568
  return { messageId };
1570
1569
  }
@@ -1598,6 +1597,7 @@ export const bitrix24Plugin = {
1598
1597
 
1599
1598
  if (mediaUrls.length > 0) {
1600
1599
  const uploadedMessageId = await uploadOutboundMedia({
1600
+ mediaService: gatewayState.mediaService,
1601
1601
  sendCtx,
1602
1602
  mediaUrls,
1603
1603
  });
@@ -1645,6 +1645,12 @@ export const bitrix24Plugin = {
1645
1645
  params: Record<string, unknown>;
1646
1646
  [key: string]: unknown;
1647
1647
  }): Promise<Record<string, unknown> | null> => {
1648
+ // Helper: wrap payload as gateway-compatible tool result
1649
+ const toolResult = (payload: Record<string, unknown>) => ({
1650
+ content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }],
1651
+ details: payload,
1652
+ });
1653
+
1648
1654
  // ─── Send with buttons ──────────────────────────────────────────────
1649
1655
  if (ctx.action === 'send') {
1650
1656
  // Only intercept send when buttons are present; otherwise let gateway handle normally
@@ -1675,11 +1681,6 @@ export const bitrix24Plugin = {
1675
1681
 
1676
1682
  const keyboard = buttons?.length ? convertButtonsToKeyboard(buttons) : undefined;
1677
1683
 
1678
- const toolResult = (payload: Record<string, unknown>) => ({
1679
- content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }],
1680
- details: payload,
1681
- });
1682
-
1683
1684
  try {
1684
1685
  const result = await gatewayState.sendService.sendText(
1685
1686
  sendCtx, message || ' ', keyboard ? { keyboard } : undefined,
@@ -1700,12 +1701,6 @@ export const bitrix24Plugin = {
1700
1701
  // ─── React ────────────────────────────────────────────────────────────
1701
1702
  if (ctx.action !== 'react') return null;
1702
1703
 
1703
- // Helper: wrap payload as gateway-compatible tool result
1704
- const toolResult = (payload: Record<string, unknown>) => ({
1705
- content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }],
1706
- details: payload,
1707
- });
1708
-
1709
1704
  const { config } = resolveAccount(ctx.cfg, ctx.accountId);
1710
1705
  if (!config.webhookUrl || !gatewayState) {
1711
1706
  return toolResult({ ok: false, reason: 'not_started', hint: 'Bitrix24 gateway not started. Do not retry.' });
@@ -1782,8 +1777,6 @@ export const bitrix24Plugin = {
1782
1777
  log?: Logger;
1783
1778
  setStatus?: (status: Record<string, unknown>) => void;
1784
1779
  }) => {
1785
- const logger = ctx.log ?? defaultLogger;
1786
-
1787
1780
  // Guard: only one account can run at a time (singleton gateway)
1788
1781
  if (gatewayState !== null) {
1789
1782
  throw new Error(
@@ -1793,6 +1786,7 @@ export const bitrix24Plugin = {
1793
1786
  }
1794
1787
 
1795
1788
  const config = getConfig(ctx.cfg, ctx.accountId);
1789
+ const logger = createVerboseLogger(ctx.log ?? defaultLogger, Boolean(config.verboseLog));
1796
1790
 
1797
1791
  if (!config.webhookUrl) {
1798
1792
  logger.warn(`[${ctx.accountId}] no webhookUrl configured, skipping`);
@@ -2114,18 +2108,12 @@ export const bitrix24Plugin = {
2114
2108
  deliver: async (payload) => {
2115
2109
  await replyStatusHeartbeat.stopAndWait();
2116
2110
  const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
2117
- for (const mediaUrl of mediaUrls) {
2118
- const uploadResult = await mediaService.uploadMediaToChat({
2119
- localPath: mediaUrl,
2120
- fileName: basename(mediaUrl),
2121
- webhookUrl,
2122
- bot,
2123
- dialogId: conversation.dialogId,
2111
+ if (mediaUrls.length > 0) {
2112
+ await uploadOutboundMedia({
2113
+ mediaService,
2114
+ sendCtx,
2115
+ mediaUrls,
2124
2116
  });
2125
-
2126
- if (!uploadResult.ok) {
2127
- throw new Error(`Failed to upload media: ${basename(mediaUrl)}`);
2128
- }
2129
2117
  }
2130
2118
  if (payload.text) {
2131
2119
  let text = payload.text;
@@ -2209,6 +2197,45 @@ export const bitrix24Plugin = {
2209
2197
  });
2210
2198
  }
2211
2199
  };
2200
+ const maybeReactToBotMessageReaction = async (reactionCtx: FetchReactionContext): Promise<void> => {
2201
+ if (reactionCtx.action !== 'set') {
2202
+ return;
2203
+ }
2204
+
2205
+ const botId = String(bot.botId);
2206
+ if (reactionCtx.senderId === botId || reactionCtx.messageAuthorId !== botId) {
2207
+ return;
2208
+ }
2209
+
2210
+ const messageId = toMessageId(reactionCtx.messageId);
2211
+ if (!messageId) {
2212
+ return;
2213
+ }
2214
+
2215
+ try {
2216
+ await api.addReaction(
2217
+ webhookUrl,
2218
+ bot,
2219
+ messageId,
2220
+ BOT_MESSAGE_WATCH_REACTION,
2221
+ );
2222
+ } catch (err) {
2223
+ const errMsg = err instanceof Error ? err.message : String(err);
2224
+ if (errMsg.includes('REACTION_ALREADY_SET')) {
2225
+ logger.debug('Watch reaction already set on bot message', {
2226
+ chatId: reactionCtx.dialogId,
2227
+ messageId: reactionCtx.messageId,
2228
+ });
2229
+ return;
2230
+ }
2231
+
2232
+ logger.warn('Failed to add watch reaction to bot message', {
2233
+ chatId: reactionCtx.dialogId,
2234
+ messageId: reactionCtx.messageId,
2235
+ error: err,
2236
+ });
2237
+ }
2238
+ };
2212
2239
  const notifyWebhookOwnerAboutWatchMatch = async (
2213
2240
  msgCtx: B24MsgContext,
2214
2241
  watchRule: Bitrix24GroupWatchConfig | Bitrix24AgentWatchConfig,
@@ -2295,6 +2322,9 @@ export const bitrix24Plugin = {
2295
2322
  const inboundHandler = new InboundHandler({
2296
2323
  config,
2297
2324
  logger,
2325
+ onReactionChange: async (reactionCtx) => {
2326
+ await maybeReactToBotMessageReaction(reactionCtx);
2327
+ },
2298
2328
 
2299
2329
  onMessage: async (msgCtx: B24MsgContext) => {
2300
2330
  logger.info('Inbound message', {
@@ -2349,6 +2379,10 @@ export const bitrix24Plugin = {
2349
2379
  ? findMatchingWatchRule(msgCtx, agentWatchRules)
2350
2380
  : undefined;
2351
2381
 
2382
+ /** Shorthand: record message in RAM history for this dialog. */
2383
+ const recordHistory = (body?: string) =>
2384
+ appendMessageToHistory({ historyCache, historyKey, historyLimit, msgCtx, body });
2385
+
2352
2386
  if (msgCtx.eventScope === 'user') {
2353
2387
  const isBotDialogUserEvent = msgCtx.isDm && msgCtx.chatId === String(bot.botId);
2354
2388
  const isBotAuthoredUserEvent = msgCtx.senderId === String(bot.botId);
@@ -2364,12 +2398,7 @@ export const bitrix24Plugin = {
2364
2398
  return;
2365
2399
  }
2366
2400
 
2367
- appendMessageToHistory({
2368
- historyCache,
2369
- historyKey,
2370
- historyLimit,
2371
- msgCtx,
2372
- });
2401
+ recordHistory();
2373
2402
 
2374
2403
  if (webhookOwnerId && msgCtx.senderId !== webhookOwnerId && agentWatchRule?.mode === 'notifyOwnerDm') {
2375
2404
  await notifyWebhookOwnerAboutWatchMatch(msgCtx, agentWatchRule);
@@ -2405,12 +2434,7 @@ export const bitrix24Plugin = {
2405
2434
  && groupAccess?.requireMention
2406
2435
  && !msgCtx.wasMentioned
2407
2436
  ) {
2408
- appendMessageToHistory({
2409
- historyCache,
2410
- historyKey,
2411
- historyLimit,
2412
- msgCtx,
2413
- });
2437
+ recordHistory();
2414
2438
 
2415
2439
  logger.info('Skipping group message without mention', {
2416
2440
  chatId: msgCtx.chatId,
@@ -2421,12 +2445,7 @@ export const bitrix24Plugin = {
2421
2445
  }
2422
2446
 
2423
2447
  if (activeWatchRule?.mode === 'notifyOwnerDm') {
2424
- appendMessageToHistory({
2425
- historyCache,
2426
- historyKey,
2427
- historyLimit,
2428
- msgCtx,
2429
- });
2448
+ recordHistory();
2430
2449
 
2431
2450
  await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeWatchRule);
2432
2451
  logger.debug('Group watch matched and notified webhook owner in DM', {
@@ -2466,12 +2485,7 @@ export const bitrix24Plugin = {
2466
2485
 
2467
2486
  if (accessResult === 'deny') {
2468
2487
  if (msgCtx.isGroup) {
2469
- appendMessageToHistory({
2470
- historyCache,
2471
- historyKey,
2472
- historyLimit,
2473
- msgCtx,
2474
- });
2488
+ recordHistory();
2475
2489
 
2476
2490
  if (!msgCtx.wasMentioned) {
2477
2491
  logger.debug('Group message blocked silently without mention', {
@@ -2507,12 +2521,7 @@ export const bitrix24Plugin = {
2507
2521
 
2508
2522
  if (accessResult === 'pairing') {
2509
2523
  if (msgCtx.isGroup) {
2510
- appendMessageToHistory({
2511
- historyCache,
2512
- historyKey,
2513
- historyLimit,
2514
- msgCtx,
2515
- });
2524
+ recordHistory();
2516
2525
 
2517
2526
  await maybeSendDialogNotice(
2518
2527
  `group-pairing:${msgCtx.chatId}:${msgCtx.senderId}`,
@@ -2659,8 +2668,6 @@ export const bitrix24Plugin = {
2659
2668
  logger.warn('Command event has invalid messageId, skipping response', { commandId, messageId, dialogId });
2660
2669
  return;
2661
2670
  }
2662
- const canMarkRead = true;
2663
-
2664
2671
  if (!isDm && groupAccess?.groupPolicy === 'disabled') {
2665
2672
  await sendService.answerCommandText(
2666
2673
  commandSendCtx,
@@ -2673,9 +2680,7 @@ export const bitrix24Plugin = {
2673
2680
  await sendService.sendStatus(sendCtx, 'IMBOT_AGENT_ACTION_THINKING', 8);
2674
2681
 
2675
2682
  if (accessResult === 'deny') {
2676
- if (canMarkRead) {
2677
- await sendService.markRead(sendCtx, commandMessageId);
2678
- }
2683
+ await sendService.markRead(sendCtx, commandMessageId);
2679
2684
  await sendService.answerCommandText(
2680
2685
  commandSendCtx,
2681
2686
  buildAccessDeniedNotice(
@@ -2693,9 +2698,7 @@ export const bitrix24Plugin = {
2693
2698
  return;
2694
2699
  }
2695
2700
 
2696
- if (canMarkRead) {
2697
- await sendService.markRead(sendCtx, commandMessageId);
2698
- }
2701
+ await sendService.markRead(sendCtx, commandMessageId);
2699
2702
 
2700
2703
  if (accessResult === 'pairing') {
2701
2704
  if (!isDm) {
@@ -38,8 +38,7 @@ const AccountSchema = z.object({
38
38
  groups: z.record(z.string(), GroupSchema).optional(),
39
39
  agentWatch: z.record(z.string(), z.array(AgentWatchRuleSchema)).optional(),
40
40
  showTyping: z.boolean().optional().default(true),
41
- streamUpdates: z.boolean().optional().default(false),
42
- updateIntervalMs: z.number().int().min(500).optional().default(10000),
41
+ verboseLog: z.boolean().optional().default(false),
43
42
  });
44
43
 
45
44
  export const Bitrix24ConfigSchema = AccountSchema.extend({
package/src/config.ts CHANGED
@@ -54,16 +54,14 @@ export function resolveAccount(
54
54
  let config = getConfig(cfg, id);
55
55
 
56
56
  // Validate and normalize config against Zod schema.
57
- if (config.webhookUrl) {
58
- const result = Bitrix24ConfigSchema.safeParse(config);
59
- if (!result.success) {
60
- const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
61
- throw new Error(`[bitrix24] Invalid config for account "${id}": ${issues}`);
62
- }
63
-
64
- config = result.data;
57
+ const result = Bitrix24ConfigSchema.safeParse(config);
58
+ if (!result.success) {
59
+ const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
60
+ throw new Error(`[bitrix24] Invalid config for account "${id}": ${issues}`);
65
61
  }
66
62
 
63
+ config = result.data;
64
+
67
65
  return {
68
66
  accountId: id,
69
67
  config,
@@ -37,14 +37,7 @@ function normalizeWatchRules<T extends { userId: string; topics?: string[]; mode
37
37
  }
38
38
 
39
39
  export function normalizeGroupEntry(entry: string): string {
40
- const normalized = String(entry).trim().toLowerCase();
41
- if (/^\d+$/.test(normalized)) {
42
- return normalized;
43
- }
44
- if (/^chat\d+$/.test(normalized)) {
45
- return normalized;
46
- }
47
- return normalized;
40
+ return String(entry).trim().toLowerCase();
48
41
  }
49
42
 
50
43
  export function normalizeGroupAllowList(entries: string[] | undefined): string[] {