@ihazz/bitrix24 1.1.7 → 1.1.9

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # @ihazz/bitrix24
2
2
 
3
3
  OpenClaw channel plugin for Bitrix24 Messenger based on Bot Platform 2.0 (`imbot.v2.*` / `im.v2.*`).
4
+ This is the official, Bitrix24-certified OpenClaw plugin for Bitrix24 Messenger.
4
5
 
5
6
  ## Current Status
6
7
 
@@ -65,7 +66,7 @@ Minimal configuration:
65
66
  "groupPolicy": "webhookUser",
66
67
  "requireMention": true,
67
68
  "historyLimit": 100,
68
- "showTyping": true,
69
+ "showTyping": true,
69
70
  "capabilities": [
70
71
  "inlineButtons",
71
72
  "reactions"
@@ -173,10 +174,10 @@ The direct-message scenario is configured independently through `dmPolicy`. Grou
173
174
  | `4. Add the bot to a group chat where it responds to all of my messages in that chat.` | The bot stores all delivered group messages in RAM history, but answers only the webhook owner and does not require mention. | `groupPolicy: "webhookUser"`, `requireMention: false` |
174
175
  | `5. Add the bot to a group chat where it responds only to my mentions in that chat.` | The bot stores all delivered group messages in RAM history, but answers only the webhook owner and only on mention. | `groupPolicy: "webhookUser"`, `requireMention: true` |
175
176
  | `6. Add the bot to a group chat where scenarios 2 through 5 apply only to users from my allowlist.` | The bot stores all delivered group messages in RAM history, but group replies are limited to the merged allowlist. | `groupPolicy: "allowlist"`, `allowFrom: ["77", "42"]`, plus `requireMention: false` or `true` |
176
- | `7. The bot can summarize or search messages from any direct or group chat it participates in.` | This is baseline behavior. All delivered messages are recorded in RAM history and can later be used for in-chat context or explicit cross-chat lookup. | No extra flag. Usually `historyLimit: 100`. Use explicit Bitrix24 chat mention like `[CHAT=520]Green Chat 15[/CHAT]` for cross-chat lookup. |
177
+ | `7. The bot can summarize recent messages from the current chat and from previously seen group chats it participates in.` | This is baseline behavior. All delivered messages are recorded in RAM history for in-chat context. Previously seen group chats can also be injected from RAM by explicit Bitrix24 chat mention. | No extra flag. Usually `historyLimit: 100`. Use explicit Bitrix24 chat mention like `[CHAT=520]Green Chat 15[/CHAT]` for group cross-chat lookup. |
177
178
  | `8. In a group chat, the bot can watch a specific user and answer only on selected topics.` | The bot records all delivered messages and may answer watched users without mention when a watch rule matches. | `groups.<chat>.watch: [{ "userId": "77", "topics": ["secret", "contract"] }]` |
178
- | `9. In a group chat, the bot can watch a specific user or all users and notify the bot owner in DM with a forwarded copy of the matched message.` | The bot records all delivered messages. When a watch rule matches, it does not answer in the group and instead sends the webhook owner a DM notice with a context URL, plus a native forwarded message copy. | `groups.<chat>.watch: [{ "userId": "*", "topics": ["incident"], "mode": "notifyOwnerDm" }]` |
179
- | `10. The bot can watch all accessible group chats for a phrase and notify the bot owner in DM with a forwarded copy of the matched message.` | The bot records all delivered group messages. A wildcard group watch applies across every allowed group chat, even when a specific chat also has its own overrides. | `groups["*"].watch: [{ "userId": "*", "topics": ["incident"], "mode": "notifyOwnerDm" }]` |
179
+ | `9. In a group chat, the bot can watch a specific user or all users and notify the bot owner in DM with a copy of the matched message.` | The bot records all delivered messages. When a watch rule matches, it does not answer in the group and instead sends the webhook owner a DM notice with a context URL, then tries a native forwarded copy and falls back to a quoted copy if forwarding is unavailable. | `groups.<chat>.watch: [{ "userId": "*", "topics": ["incident"], "mode": "notifyOwnerDm" }]` |
180
+ | `10. The bot can watch all accessible group chats for a phrase and notify the bot owner in DM with a copy of the matched message.` | The bot records all delivered group messages. A wildcard group watch applies across every allowed group chat, even when a specific chat also has its own overrides. Delivery uses the same native-forward-first flow with quoted fallback when forwarding is unavailable. | `groups["*"].watch: [{ "userId": "*", "topics": ["incident"], "mode": "notifyOwnerDm" }]` |
180
181
 
181
182
  Default profile:
182
183
 
@@ -323,7 +324,7 @@ For a matching `watch` rule:
323
324
  - the sender is matched by Bitrix24 user ID; use `"*"` to match any sender
324
325
  - `topics` are matched by normalized text inclusion / token prefix matching
325
326
  - `mode: "reply"` answers in the group even if the normal group policy would otherwise require a mention
326
- - `mode: "notifyOwnerDm"` sends the webhook owner a DM notice with a user mention and a deep link back to the original chat context, plus a native forwarded copy of the matched message instead of replying in the group
327
+ - `mode: "notifyOwnerDm"` sends the webhook owner a DM notice with a deep link back to the original chat context, then tries a native forwarded copy of the matched message and falls back to a quoted copy if forwarding is unavailable
327
328
  - `groups["*"].watch` applies the same rule set to every allowed group chat
328
329
  - if a specific group also defines `watch`, those chat-specific rules are checked first and the wildcard rules remain available as a fallback
329
330
 
@@ -364,7 +365,7 @@ If `mode` is omitted, `reply` is used.
364
365
  - Forwarded-message hydration from Bitrix24 message context, including merge with buffered neighboring context when related events arrive close together
365
366
  - Cross-chat lookup from RAM memory by explicit Bitrix24 chat mention like `[CHAT=520]Green Chat 15[/CHAT]`
366
367
  - Topic watches for specific group users
367
- - Owner-DM watch notifications with native forwarded messages
368
+ - Owner-DM watch notifications with native forward and quoted fallback
368
369
  - Agent-mode ingestion of user-visible messages through combined polling
369
370
  - Agent-mode owner-DM watch notifications through `agentWatch`
370
371
 
@@ -377,13 +378,13 @@ Arbitrary local server paths are rejected on purpose for safety.
377
378
  ## Not Supported
378
379
 
379
380
  - Persistent SQLite or search-based history layer
380
- - Persistent reply or forward search across chats beyond what Bitrix24 `Chat.Message.get` / `getContext` can return
381
- - “Reply to me in DM because this started in a group” behavior
381
+ - Persistent cross-chat reply or forward retrieval beyond current RAM history and what Bitrix24 `Chat.Message.get` / `getContext` can return
382
+ - Generic reply to me in DM because this started in a group” behavior
382
383
  - Sending replies as the user through `im.v2.Chat.Message.*`
383
- - Agent-triggered user auto-replies or one-shot autoresponder flows
384
+ - Agent-mode user-event auto-replies or one-shot autoresponder flows
384
385
  - Production-ready multi-account operation in one process
385
386
 
386
- There is already account-config scaffolding in the plugin, but the current production recommendation is still one active Bitrix24 portal per instance.
387
+ There is already account-config scaffolding in the plugin, but the runtime still enforces a singleton Bitrix24 gateway per process, so the current production recommendation remains one active Bitrix24 portal per instance.
387
388
 
388
389
  ## Verification
389
390
 
@@ -34,7 +34,12 @@ export declare function resolveConversationRef(params: {
34
34
  dialogId: string;
35
35
  isDirect: boolean;
36
36
  }): Bitrix24ConversationRef;
37
- export declare function buildConversationSessionKey(routeSessionKey: string, conversation: Pick<Bitrix24ConversationRef, 'address'>): string;
37
+ export declare function buildConversationSessionKey(routeSessionKey: string, conversation: Pick<Bitrix24ConversationRef, 'address'>, sessionNamespace?: string): string;
38
+ type ActiveSessionNamespaceEntry = {
39
+ namespace: string;
40
+ updatedAt: number;
41
+ };
42
+ export declare function normalizeActiveSessionNamespaceState(state: unknown, accountId: string, now?: number): Record<string, ActiveSessionNamespaceEntry>;
38
43
  /** State held per running gateway instance */
39
44
  interface GatewayState {
40
45
  accountId: string;
@@ -46,9 +51,11 @@ interface GatewayState {
46
51
  eventMode: 'fetch' | 'webhook';
47
52
  }
48
53
  export declare function __setGatewayStateForTests(state: GatewayState | null): void;
54
+ export declare function buildWelcomeKeyboard(language?: string): B24Keyboard;
49
55
  /** Default keyboard shown with command responses and welcome messages. */
50
56
  export declare function buildDefaultCommandKeyboard(language?: string): B24Keyboard;
51
57
  export declare const DEFAULT_COMMAND_KEYBOARD: B24Keyboard;
58
+ export declare const DEFAULT_WELCOME_KEYBOARD: B24Keyboard;
52
59
  /** Generic button format used by OpenClaw channelData. */
53
60
  export interface ChannelButton {
54
61
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
@@ -1,18 +1,20 @@
1
- import { createHash } from 'node:crypto';
2
- import { basename } from 'node:path';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join } from 'node:path';
3
4
  import { listAccountIds, resolveAccount, getConfig } from './config.js';
4
5
  import { Bitrix24Api } from './api.js';
5
6
  import { SendService } from './send-service.js';
6
7
  import { MediaService } from './media-service.js';
7
8
  import { InboundHandler } from './inbound-handler.js';
8
9
  import { PollingService } from './polling-service.js';
10
+ import { resolvePollingStateDir } from './state-paths.js';
9
11
  import { normalizeAllowEntry, normalizeAllowList, checkAccessWithPairing, getWebhookUserId, } from './access-control.js';
10
12
  import { checkGroupAccessPassive, checkGroupAccessWithPairing, resolveAgentWatchRules, resolveGroupAccess, } from './group-access.js';
11
13
  import { DEFAULT_AVATAR_BASE64 } from './bot-avatar.js';
12
14
  import { Bitrix24ApiError, createVerboseLogger, defaultLogger, CHANNEL_PREFIX_RE } from './utils.js';
13
15
  import { getBitrix24Runtime } from './runtime.js';
14
- import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply } from './commands.js';
15
- import { accessApproved, accessDenied, commandKeyboardLabels, groupPairingPending, mediaDownloadFailed, groupChatUnsupported, onboardingMessage, ownerAndAllowedUsersOnly, personalBotOwnerOnly, replyGenerationFailed, watchOwnerDmNotice, } from './i18n.js';
16
+ import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply, getCommandRegistrationPayload, } from './commands.js';
17
+ import { accessApproved, accessDenied, commandKeyboardLabels, groupPairingPending, mediaDownloadFailed, groupChatUnsupported, newSessionReplyTexts, onboardingMessage, normalizeNewSessionReply, ownerAndAllowedUsersOnly, personalBotOwnerOnly, replyGenerationFailed, welcomeKeyboardLabels, watchOwnerDmNotice, } from './i18n.js';
16
18
  import { HistoryCache } from './history-cache.js';
17
19
  const PHASE_STATUS_DURATION_SECONDS = 8;
18
20
  const PHASE_STATUS_REFRESH_GRACE_MS = 1000;
@@ -31,6 +33,9 @@ const CROSS_CHAT_HISTORY_LIMIT = 20;
31
33
  const ACCESS_DENIED_REACTION = 'crossMark';
32
34
  const BOT_MESSAGE_WATCH_REACTION = 'eyes';
33
35
  const FORWARDED_CONTEXT_RANGE = 5;
36
+ const ACTIVE_SESSION_NAMESPACE_TTL_MS = 180 * 24 * 60 * 60 * 1000;
37
+ const ACTIVE_SESSION_NAMESPACE_MAX_KEYS = 1000;
38
+ const ACTIVE_SESSION_NAMESPACE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
34
39
  const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
35
40
  // ─── Emoji → B24 reaction code mapping ──────────────────────────────────
36
41
  // B24 uses named reaction codes, not Unicode emoji.
@@ -125,7 +130,7 @@ function buildTopicsBbCode(topics) {
125
130
  .join(', ');
126
131
  }
127
132
  function formatQuoteTimestamp(timestamp, language) {
128
- const locale = (language ?? 'ru').toLowerCase().slice(0, 2);
133
+ const locale = (language ?? 'en').toLowerCase().slice(0, 2);
129
134
  const value = timestamp ?? Date.now();
130
135
  try {
131
136
  return new Intl.DateTimeFormat(locale, {
@@ -137,7 +142,7 @@ function formatQuoteTimestamp(timestamp, language) {
137
142
  }).format(new Date(value)).replace(',', '');
138
143
  }
139
144
  catch {
140
- return new Intl.DateTimeFormat('ru', {
145
+ return new Intl.DateTimeFormat('en', {
141
146
  year: 'numeric',
142
147
  month: '2-digit',
143
148
  day: '2-digit',
@@ -146,6 +151,12 @@ function formatQuoteTimestamp(timestamp, language) {
146
151
  }).format(new Date(value)).replace(',', '');
147
152
  }
148
153
  }
154
+ function buildWatchQuoteAnchor(msgCtx, ownerId) {
155
+ if (msgCtx.isGroup) {
156
+ return `#${msgCtx.chatId}/${msgCtx.messageId}`;
157
+ }
158
+ return `#${msgCtx.chatId}:${ownerId}/${msgCtx.messageId}`;
159
+ }
149
160
  function buildWatchQuoteText(params) {
150
161
  const separator = '------------------------------------------------------';
151
162
  const senderLine = `${escapeBbCodeText(params.senderName)} [${formatQuoteTimestamp(params.timestamp, params.language)}] ${params.anchor}`;
@@ -349,8 +360,81 @@ export function resolveConversationRef(params) {
349
360
  },
350
361
  };
351
362
  }
352
- export function buildConversationSessionKey(routeSessionKey, conversation) {
353
- return `${routeSessionKey}:${conversation.address}`;
363
+ export function buildConversationSessionKey(routeSessionKey, conversation, sessionNamespace) {
364
+ const baseKey = `${routeSessionKey}:${conversation.address}`;
365
+ return sessionNamespace
366
+ ? `${baseKey}:${sessionNamespace}`
367
+ : baseKey;
368
+ }
369
+ function normalizeSessionNamespaceTimestamp(value, now, fallback) {
370
+ let parsed;
371
+ if (typeof value === 'number') {
372
+ parsed = value;
373
+ }
374
+ else if (typeof value === 'string') {
375
+ const asNumber = Number(value);
376
+ parsed = Number.isFinite(asNumber) ? asNumber : Date.parse(value);
377
+ }
378
+ if (!Number.isFinite(parsed) || parsed == null || parsed <= 0) {
379
+ return fallback;
380
+ }
381
+ return Math.min(parsed, now);
382
+ }
383
+ function isValidActiveSessionNamespace(value) {
384
+ return typeof value === 'string' && ACTIVE_SESSION_NAMESPACE_RE.test(value);
385
+ }
386
+ export function normalizeActiveSessionNamespaceState(state, accountId, now = Date.now()) {
387
+ if (!state || typeof state !== 'object') {
388
+ return {};
389
+ }
390
+ const rawState = state;
391
+ const fallbackUpdatedAt = normalizeSessionNamespaceTimestamp(rawState.updatedAt, now, now);
392
+ const accountKeyPrefix = `${accountId}:`;
393
+ const entries = [];
394
+ for (const [key, value] of Object.entries(rawState.sessions ?? {})) {
395
+ if (!key.startsWith(accountKeyPrefix)) {
396
+ continue;
397
+ }
398
+ let namespace;
399
+ let updatedAt = fallbackUpdatedAt;
400
+ if (typeof value === 'string') {
401
+ namespace = value;
402
+ }
403
+ else if (value && typeof value === 'object') {
404
+ const rawEntry = value;
405
+ namespace = rawEntry.namespace;
406
+ updatedAt = normalizeSessionNamespaceTimestamp(rawEntry.updatedAt, now, fallbackUpdatedAt);
407
+ }
408
+ if (!isValidActiveSessionNamespace(namespace)) {
409
+ continue;
410
+ }
411
+ if (now - updatedAt > ACTIVE_SESSION_NAMESPACE_TTL_MS) {
412
+ continue;
413
+ }
414
+ entries.push([key, { namespace, updatedAt }]);
415
+ }
416
+ entries.sort((left, right) => left[1].updatedAt - right[1].updatedAt);
417
+ return Object.fromEntries(entries.slice(-ACTIVE_SESSION_NAMESPACE_MAX_KEYS));
418
+ }
419
+ function pruneActiveSessionNamespaceEntries(entries, now = Date.now()) {
420
+ for (const [key, value] of entries) {
421
+ if (!isValidActiveSessionNamespace(value?.namespace)
422
+ || !Number.isFinite(value?.updatedAt)
423
+ || value.updatedAt <= 0
424
+ || now - value.updatedAt > ACTIVE_SESSION_NAMESPACE_TTL_MS) {
425
+ entries.delete(key);
426
+ }
427
+ }
428
+ if (entries.size <= ACTIVE_SESSION_NAMESPACE_MAX_KEYS) {
429
+ return;
430
+ }
431
+ const overflow = entries.size - ACTIVE_SESSION_NAMESPACE_MAX_KEYS;
432
+ const staleFirst = [...entries.entries()]
433
+ .sort((left, right) => left[1].updatedAt - right[1].updatedAt)
434
+ .slice(0, overflow);
435
+ for (const [key] of staleFirst) {
436
+ entries.delete(key);
437
+ }
354
438
  }
355
439
  function buildHistoryBody(msgCtx) {
356
440
  const text = msgCtx.text.trim();
@@ -518,7 +602,7 @@ function normalizeTopicText(text) {
518
602
  }
519
603
  function tokenizeTopicText(text) {
520
604
  return normalizeTopicText(text)
521
- .split(/[^a-zа-яё0-9]+/i)
605
+ .split(/[^\p{L}\p{N}]+/u)
522
606
  .filter(Boolean);
523
607
  }
524
608
  function matchesWatchTopic(messageText, topic) {
@@ -654,7 +738,18 @@ let gatewayState = null;
654
738
  export function __setGatewayStateForTests(state) {
655
739
  gatewayState = state;
656
740
  }
657
- // ─── Default command keyboard ────────────────────────────────────────────────
741
+ // ─── Keyboard layouts ────────────────────────────────────────────────────────
742
+ export function buildWelcomeKeyboard(language) {
743
+ const labels = welcomeKeyboardLabels(language);
744
+ return [
745
+ { TEXT: labels.todayTasks, ACTION: 'SEND', ACTION_VALUE: labels.todayTasks, DISPLAY: 'LINE' },
746
+ { TEXT: labels.stalledDeals, ACTION: 'SEND', ACTION_VALUE: labels.stalledDeals, DISPLAY: 'LINE' },
747
+ { TYPE: 'NEWLINE' },
748
+ { TEXT: labels.newSession, COMMAND: 'new', DISPLAY: 'LINE' },
749
+ { TEXT: labels.commands, COMMAND: 'commands', DISPLAY: 'LINE' },
750
+ { TEXT: labels.help, COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
751
+ ];
752
+ }
658
753
  /** Default keyboard shown with command responses and welcome messages. */
659
754
  export function buildDefaultCommandKeyboard(language) {
660
755
  const labels = commandKeyboardLabels(language);
@@ -668,6 +763,7 @@ export function buildDefaultCommandKeyboard(language) {
668
763
  ];
669
764
  }
670
765
  export const DEFAULT_COMMAND_KEYBOARD = buildDefaultCommandKeyboard();
766
+ export const DEFAULT_WELCOME_KEYBOARD = buildWelcomeKeyboard();
671
767
  function parseRegisteredCommandTrigger(callbackData) {
672
768
  const trimmed = callbackData.trim();
673
769
  const isSlashCommand = trimmed.startsWith('/');
@@ -739,7 +835,8 @@ export function extractKeyboardFromPayload(payload) {
739
835
  }
740
836
  const tgData = cd.telegram;
741
837
  if (tgData?.buttons?.length) {
742
- return convertButtonsToKeyboard(tgData.buttons);
838
+ const keyboard = convertButtonsToKeyboard(tgData.buttons);
839
+ return keyboard.length > 0 ? keyboard : undefined;
743
840
  }
744
841
  return undefined;
745
842
  }
@@ -781,6 +878,12 @@ function normalizeCommandReplyPayload(params) {
781
878
  return { text: formattedText, convertMarkdown: false };
782
879
  }
783
880
  }
881
+ if (commandName === 'new' && commandParams.trim() === '') {
882
+ const normalizedText = normalizeNewSessionReply(language, text);
883
+ if (normalizedText) {
884
+ return { text: normalizedText, convertMarkdown: false };
885
+ }
886
+ }
784
887
  return { text };
785
888
  }
786
889
  /**
@@ -835,7 +938,7 @@ async function sendInitialWelcomeToWebhookOwner(params) {
835
938
  const text = onboardingMessage(language, config.botName ?? 'OpenClaw', config.dmPolicy);
836
939
  const options = isPairing
837
940
  ? undefined
838
- : { keyboard: buildDefaultCommandKeyboard(language) };
941
+ : { keyboard: buildWelcomeKeyboard(language) };
839
942
  try {
840
943
  await sendService.sendText(sendCtx, text, options);
841
944
  welcomedDialogs.add(ownerId);
@@ -942,8 +1045,7 @@ async function ensureCommandsRegistered(api, config, bot, logger) {
942
1045
  try {
943
1046
  await api.registerCommand(webhookUrl, bot, {
944
1047
  command: cmd.command,
945
- title: { en: cmd.en, ru: cmd.ru },
946
- ...(cmd.params ? { params: { en: cmd.params, ru: cmd.params } } : {}),
1048
+ ...getCommandRegistrationPayload(cmd),
947
1049
  });
948
1050
  registered++;
949
1051
  }
@@ -1339,6 +1441,72 @@ export const bitrix24Plugin = {
1339
1441
  const welcomedDialogs = new Set();
1340
1442
  const dialogNoticeTimestamps = new Map();
1341
1443
  const historyCache = new HistoryCache({ maxKeys: HISTORY_CACHE_MAX_KEYS });
1444
+ const activeSessionNamespaces = new Map();
1445
+ const sessionNamespaceStatePath = join(resolvePollingStateDir(), `session-namespaces-${ctx.accountId}.json`);
1446
+ let persistActiveSessionNamespacesTail = Promise.resolve();
1447
+ const buildActiveSessionNamespaceKey = (conversation) => {
1448
+ return `${ctx.accountId}:${conversation.address}`;
1449
+ };
1450
+ const loadActiveSessionNamespaces = async () => {
1451
+ try {
1452
+ const raw = await readFile(sessionNamespaceStatePath, 'utf-8');
1453
+ const state = JSON.parse(raw);
1454
+ const entries = normalizeActiveSessionNamespaceState(state, ctx.accountId);
1455
+ const persistedSessions = state.sessions ?? {};
1456
+ const needsCompaction = Object.keys(entries).length !== Object.keys(persistedSessions).length
1457
+ || Object.values(persistedSessions).some((value) => typeof value === 'string');
1458
+ activeSessionNamespaces.clear();
1459
+ for (const [key, value] of Object.entries(entries)) {
1460
+ activeSessionNamespaces.set(key, value);
1461
+ }
1462
+ if (needsCompaction) {
1463
+ void persistActiveSessionNamespaces();
1464
+ }
1465
+ }
1466
+ catch (err) {
1467
+ logger.debug('Failed to load active session namespaces, starting fresh', err);
1468
+ }
1469
+ };
1470
+ const persistActiveSessionNamespaces = () => {
1471
+ persistActiveSessionNamespacesTail = persistActiveSessionNamespacesTail
1472
+ .catch(() => undefined)
1473
+ .then(async () => {
1474
+ try {
1475
+ pruneActiveSessionNamespaceEntries(activeSessionNamespaces);
1476
+ await mkdir(dirname(sessionNamespaceStatePath), { recursive: true });
1477
+ const data = JSON.stringify({
1478
+ updatedAt: new Date().toISOString(),
1479
+ sessions: Object.fromEntries(activeSessionNamespaces),
1480
+ }, null, 2);
1481
+ const tmpPath = `${sessionNamespaceStatePath}.${randomUUID()}.tmp`;
1482
+ await writeFile(tmpPath, data, 'utf-8');
1483
+ await rename(tmpPath, sessionNamespaceStatePath);
1484
+ }
1485
+ catch (err) {
1486
+ logger.warn('Failed to persist active session namespaces', err);
1487
+ }
1488
+ });
1489
+ return persistActiveSessionNamespacesTail;
1490
+ };
1491
+ const resolveActiveConversationSessionKey = (routeSessionKey, conversation) => {
1492
+ return buildConversationSessionKey(routeSessionKey, conversation, activeSessionNamespaces.get(buildActiveSessionNamespaceKey(conversation))?.namespace);
1493
+ };
1494
+ const startNewConversationSession = async (conversation) => {
1495
+ const sessionNamespace = randomUUID();
1496
+ pruneActiveSessionNamespaceEntries(activeSessionNamespaces);
1497
+ historyCache.clear(conversation.historyKey);
1498
+ activeSessionNamespaces.set(buildActiveSessionNamespaceKey(conversation), {
1499
+ namespace: sessionNamespace,
1500
+ updatedAt: Date.now(),
1501
+ });
1502
+ await persistActiveSessionNamespaces();
1503
+ logger.info('Started new local conversation session', {
1504
+ dialogId: conversation.dialogId,
1505
+ sessionNamespace,
1506
+ });
1507
+ return sessionNamespace;
1508
+ };
1509
+ await loadActiveSessionNamespaces();
1342
1510
  // Cleanup stale denied dialog entries once per day
1343
1511
  const DENIED_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
1344
1512
  const deniedCleanupTimer = setInterval(() => {
@@ -1575,7 +1743,7 @@ export const bitrix24Plugin = {
1575
1743
  RawBody: body,
1576
1744
  From: conversation.address,
1577
1745
  To: conversation.address,
1578
- SessionKey: buildConversationSessionKey(route.sessionKey, conversation),
1746
+ SessionKey: resolveActiveConversationSessionKey(route.sessionKey, conversation),
1579
1747
  AccountId: route.accountId,
1580
1748
  ChatType: msgCtx.isDm ? 'direct' : 'group',
1581
1749
  ConversationLabel: msgCtx.senderName,
@@ -1737,6 +1905,18 @@ export const bitrix24Plugin = {
1737
1905
  bot,
1738
1906
  dialogId: ownerId,
1739
1907
  };
1908
+ const sendQuotedWatchMessage = async () => {
1909
+ const quoteText = buildWatchQuoteText({
1910
+ senderName: msgCtx.senderName || msgCtx.chatName || msgCtx.chatId,
1911
+ language: msgCtx.language,
1912
+ timestamp: msgCtx.timestamp,
1913
+ anchor: buildWatchQuoteAnchor(msgCtx, ownerId),
1914
+ body: msgCtx.text.trim(),
1915
+ });
1916
+ await sendService.sendText(ownerSendCtx, quoteText, {
1917
+ convertMarkdown: false,
1918
+ });
1919
+ };
1740
1920
  const noticeText = watchOwnerDmNotice(msgCtx.language, {
1741
1921
  chatRef: buildChatContextUrl(msgCtx.chatId, msgCtx.messageId, msgCtx.isDm
1742
1922
  ? (msgCtx.senderName || msgCtx.chatName || msgCtx.chatId)
@@ -1748,24 +1928,17 @@ export const bitrix24Plugin = {
1748
1928
  await sendService.sendText(ownerSendCtx, noticeText, {
1749
1929
  convertMarkdown: false,
1750
1930
  });
1751
- if (msgCtx.eventScope === 'user' && msgCtx.isDm) {
1752
- const quoteText = buildWatchQuoteText({
1753
- senderName: msgCtx.senderName || msgCtx.chatName || msgCtx.chatId,
1754
- language: msgCtx.language,
1755
- timestamp: msgCtx.timestamp,
1756
- anchor: `#${msgCtx.chatId}:${ownerId}/${msgCtx.messageId}`,
1757
- body: msgCtx.text.trim(),
1758
- });
1759
- await sendService.sendText(ownerSendCtx, quoteText, {
1760
- convertMarkdown: false,
1761
- });
1762
- return true;
1931
+ try {
1932
+ await api.sendMessage(webhookUrl, bot, ownerId, null, { forwardMessages: [forwardedMessageId] });
1933
+ }
1934
+ catch (err) {
1935
+ logger.warn('Failed to send owner watch notification with native forward, falling back to quote', err);
1936
+ await sendQuotedWatchMessage();
1763
1937
  }
1764
- await api.sendMessage(webhookUrl, bot, ownerId, null, { forwardMessages: [forwardedMessageId] });
1765
1938
  return true;
1766
1939
  }
1767
1940
  catch (err) {
1768
- logger.warn('Failed to send owner watch notification with native forward', err);
1941
+ logger.warn('Failed to send owner watch notification', err);
1769
1942
  return false;
1770
1943
  }
1771
1944
  };
@@ -1988,9 +2161,10 @@ export const bitrix24Plugin = {
1988
2161
  onCommand: async (cmdCtx) => {
1989
2162
  const { commandId, commandName, commandParams, commandText, senderId, dialogId, chatId, chatType, messageId, } = cmdCtx;
1990
2163
  const isDm = chatType === 'P';
2164
+ const replyDialogId = isDm ? senderId : dialogId;
1991
2165
  const conversation = resolveConversationRef({
1992
2166
  accountId: ctx.accountId,
1993
- dialogId,
2167
+ dialogId: replyDialogId,
1994
2168
  isDirect: isDm,
1995
2169
  });
1996
2170
  logger.info('Inbound command', {
@@ -2006,7 +2180,7 @@ export const bitrix24Plugin = {
2006
2180
  const sendCtx = {
2007
2181
  webhookUrl,
2008
2182
  bot,
2009
- dialogId: conversation.dialogId,
2183
+ dialogId: replyDialogId,
2010
2184
  };
2011
2185
  let runtime;
2012
2186
  let cfg;
@@ -2078,7 +2252,6 @@ export const bitrix24Plugin = {
2078
2252
  await sendService.answerCommandText(commandSendCtx, groupChatUnsupported(cmdCtx.language), { convertMarkdown: false });
2079
2253
  return;
2080
2254
  }
2081
- await sendService.sendStatus(sendCtx, 'IMBOT_AGENT_ACTION_THINKING', 8);
2082
2255
  if (accessResult === 'deny') {
2083
2256
  await sendService.markRead(sendCtx, commandMessageId);
2084
2257
  await sendService.answerCommandText(commandSendCtx, buildAccessDeniedNotice(cmdCtx.language, isDm ? config.dmPolicy : groupAccess?.groupPolicy, {
@@ -2099,6 +2272,9 @@ export const bitrix24Plugin = {
2099
2272
  }
2100
2273
  await directTextCoalescer.flush(ctx.accountId, conversation.dialogId);
2101
2274
  const defaultCommandKeyboard = buildDefaultCommandKeyboard(cmdCtx.language);
2275
+ const defaultSessionKeyboard = commandName === 'new' && commandParams.trim() === ''
2276
+ ? buildWelcomeKeyboard(cmdCtx.language)
2277
+ : defaultCommandKeyboard;
2102
2278
  if (commandName === 'help' || commandName === 'commands') {
2103
2279
  const helpText = buildCommandsHelpText(cmdCtx.language, { concise: commandName === 'help' });
2104
2280
  if (isDm) {
@@ -2109,6 +2285,18 @@ export const bitrix24Plugin = {
2109
2285
  }
2110
2286
  return;
2111
2287
  }
2288
+ if (commandName === 'new' && commandParams.trim() === '') {
2289
+ await startNewConversationSession(conversation);
2290
+ const startedText = newSessionReplyTexts(cmdCtx.language).started;
2291
+ if (isDm) {
2292
+ await sendService.sendText(sendCtx, startedText, { keyboard: defaultSessionKeyboard, convertMarkdown: false });
2293
+ }
2294
+ else {
2295
+ await sendService.answerCommandText(commandSendCtx, startedText, { keyboard: defaultSessionKeyboard, convertMarkdown: false });
2296
+ }
2297
+ return;
2298
+ }
2299
+ await sendService.sendStatus(sendCtx, 'IMBOT_AGENT_ACTION_THINKING', 8);
2112
2300
  const route = runtime.channel.routing.resolveAgentRoute({
2113
2301
  cfg,
2114
2302
  channel: 'bitrix24',
@@ -2123,7 +2311,7 @@ export const bitrix24Plugin = {
2123
2311
  CommandBody: commandText,
2124
2312
  CommandAuthorized: true,
2125
2313
  CommandSource: 'native',
2126
- CommandTargetSessionKey: buildConversationSessionKey(route.sessionKey, conversation),
2314
+ CommandTargetSessionKey: resolveActiveConversationSessionKey(route.sessionKey, conversation),
2127
2315
  From: conversation.address,
2128
2316
  To: `slash:${senderId}`,
2129
2317
  SessionKey: slashSessionKey,
@@ -2157,7 +2345,7 @@ export const bitrix24Plugin = {
2157
2345
  await replyStatusHeartbeat.stopAndWait();
2158
2346
  if (payload.text) {
2159
2347
  const keyboard = extractKeyboardFromPayload(payload)
2160
- ?? defaultCommandKeyboard;
2348
+ ?? defaultSessionKeyboard;
2161
2349
  const formattedPayload = normalizeCommandReplyPayload({
2162
2350
  commandName,
2163
2351
  commandParams,
@@ -2200,13 +2388,13 @@ export const bitrix24Plugin = {
2200
2388
  const fallbackText = replyGenerationFailed(cmdCtx.language);
2201
2389
  if (isDm) {
2202
2390
  await sendService.sendText(sendCtx, fallbackText, {
2203
- keyboard: defaultCommandKeyboard,
2391
+ keyboard: defaultSessionKeyboard,
2204
2392
  convertMarkdown: false,
2205
2393
  });
2206
2394
  }
2207
2395
  else {
2208
2396
  await sendService.answerCommandText(commandSendCtx, fallbackText, {
2209
- keyboard: defaultCommandKeyboard,
2397
+ keyboard: defaultSessionKeyboard,
2210
2398
  convertMarkdown: false,
2211
2399
  });
2212
2400
  }
@@ -2223,13 +2411,13 @@ export const bitrix24Plugin = {
2223
2411
  const fallbackText = replyGenerationFailed(cmdCtx.language);
2224
2412
  if (isDm) {
2225
2413
  await sendService.sendText(sendCtx, fallbackText, {
2226
- keyboard: defaultCommandKeyboard,
2414
+ keyboard: defaultSessionKeyboard,
2227
2415
  convertMarkdown: false,
2228
2416
  });
2229
2417
  }
2230
2418
  else {
2231
2419
  await sendService.answerCommandText(commandSendCtx, fallbackText, {
2232
- keyboard: defaultCommandKeyboard,
2420
+ keyboard: defaultSessionKeyboard,
2233
2421
  convertMarkdown: false,
2234
2422
  });
2235
2423
  }
@@ -2312,7 +2500,7 @@ export const bitrix24Plugin = {
2312
2500
  const isPairing = config.dmPolicy === 'pairing';
2313
2501
  const text = onboardingMessage(language, config.botName ?? 'OpenClaw', config.dmPolicy);
2314
2502
  try {
2315
- await sendService.sendText(sendCtx, text, isPairing ? undefined : { keyboard: buildDefaultCommandKeyboard(language) });
2503
+ await sendService.sendText(sendCtx, text, isPairing ? undefined : { keyboard: buildWelcomeKeyboard(language) });
2316
2504
  welcomedDialogs.add(dialogId);
2317
2505
  logger.info('Welcome message sent', { dialogId });
2318
2506
  }