@ihazz/bitrix24 1.1.7 → 1.1.8
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/dist/src/channel.d.ts +8 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +227 -39
- package/dist/src/channel.js.map +1 -1
- package/dist/src/commands.d.ts +7 -4
- package/dist/src/commands.d.ts.map +1 -1
- package/dist/src/commands.js +57 -75
- package/dist/src/commands.js.map +1 -1
- package/dist/src/i18n.d.ts +33 -0
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +448 -0
- package/dist/src/i18n.js.map +1 -1
- package/package.json +1 -1
- package/src/channel.ts +307 -46
- package/src/commands.ts +75 -81
- package/src/i18n.ts +512 -0
package/dist/src/channel.d.ts
CHANGED
|
@@ -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'
|
|
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":"
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/src/channel.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
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 ?? '
|
|
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('
|
|
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
|
-
|
|
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(/[
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
??
|
|
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:
|
|
2391
|
+
keyboard: defaultSessionKeyboard,
|
|
2204
2392
|
convertMarkdown: false,
|
|
2205
2393
|
});
|
|
2206
2394
|
}
|
|
2207
2395
|
else {
|
|
2208
2396
|
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
2209
|
-
keyboard:
|
|
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:
|
|
2414
|
+
keyboard: defaultSessionKeyboard,
|
|
2227
2415
|
convertMarkdown: false,
|
|
2228
2416
|
});
|
|
2229
2417
|
}
|
|
2230
2418
|
else {
|
|
2231
2419
|
await sendService.answerCommandText(commandSendCtx, fallbackText, {
|
|
2232
|
-
keyboard:
|
|
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:
|
|
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
|
}
|