@ihazz/bitrix24 1.1.11 → 1.1.13
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 +77 -4
- package/dist/src/api.d.ts +10 -5
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +42 -8
- package/dist/src/api.js.map +1 -1
- package/dist/src/channel.d.ts +18 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +1274 -71
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.d.ts +0 -1
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +68 -79
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/inbound-handler.js +85 -7
- package/dist/src/inbound-handler.js.map +1 -1
- package/dist/src/media-service.d.ts +2 -0
- package/dist/src/media-service.d.ts.map +1 -1
- package/dist/src/media-service.js +117 -14
- package/dist/src/media-service.js.map +1 -1
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/message-utils.js +73 -3
- package/dist/src/message-utils.js.map +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/send-service.d.ts +1 -0
- package/dist/src/send-service.d.ts.map +1 -1
- package/dist/src/send-service.js +26 -3
- package/dist/src/send-service.js.map +1 -1
- package/dist/src/state-paths.d.ts +1 -0
- package/dist/src/state-paths.d.ts.map +1 -1
- package/dist/src/state-paths.js +9 -0
- package/dist/src/state-paths.js.map +1 -1
- package/dist/src/types.d.ts +92 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +62 -13
- package/src/channel.ts +1739 -96
- package/src/i18n.ts +68 -81
- package/src/inbound-handler.ts +110 -7
- package/src/media-service.ts +146 -15
- package/src/message-utils.ts +90 -3
- package/src/runtime.ts +1 -0
- package/src/send-service.ts +40 -2
- package/src/state-paths.ts +11 -0
- package/src/types.ts +122 -0
package/src/media-service.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isIP } from 'node:net';
|
|
3
3
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
-
import { writeFile, mkdir, stat, unlink, rename, realpath } from 'node:fs/promises';
|
|
5
|
-
import { join, basename, relative, sep } from 'node:path';
|
|
4
|
+
import { copyFile, writeFile, mkdir, stat, unlink, rename, realpath } from 'node:fs/promises';
|
|
5
|
+
import { isAbsolute, join, basename, relative, resolve as resolvePath, sep } from 'node:path';
|
|
6
6
|
import { Readable, Transform } from 'node:stream';
|
|
7
7
|
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { Bitrix24Api } from './api.js';
|
|
9
10
|
import type { BotContext } from './api.js';
|
|
10
11
|
import type { Logger } from './types.js';
|
|
11
|
-
import { resolveManagedMediaDir } from './state-paths.js';
|
|
12
|
+
import { resolveManagedMediaDir, resolveTrustedWorkspaceDirs } from './state-paths.js';
|
|
12
13
|
import { defaultLogger, maskUrlForLog, serializeError } from './utils.js';
|
|
13
14
|
|
|
14
15
|
export interface DownloadedMedia {
|
|
@@ -22,6 +23,11 @@ export interface UploadedMedia {
|
|
|
22
23
|
messageId?: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
type ResolvedUploadPath = {
|
|
27
|
+
path: string;
|
|
28
|
+
cleanup: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
25
31
|
const MIME_MAP: Record<string, string> = {
|
|
26
32
|
jpg: 'image/jpeg',
|
|
27
33
|
jpeg: 'image/jpeg',
|
|
@@ -66,6 +72,11 @@ function mimeFromExtension(ext: string): string {
|
|
|
66
72
|
return MIME_MAP[ext.toLowerCase()] ?? 'application/octet-stream';
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
function isPathInside(rootPath: string, filePath: string): boolean {
|
|
76
|
+
const rel = relative(rootPath, filePath);
|
|
77
|
+
return !rel.startsWith('..') && !rel.startsWith(sep);
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
function normalizeResponseContentType(contentType: string | null): string | undefined {
|
|
70
81
|
if (!contentType) {
|
|
71
82
|
return undefined;
|
|
@@ -75,6 +86,23 @@ function normalizeResponseContentType(contentType: string | null): string | unde
|
|
|
75
86
|
return normalized ? normalized : undefined;
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
function normalizeLocalUploadPath(localPath: string): string | null {
|
|
90
|
+
const trimmed = localPath.trim();
|
|
91
|
+
if (!trimmed) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (trimmed.startsWith('file://')) {
|
|
96
|
+
try {
|
|
97
|
+
return fileURLToPath(trimmed);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
105
|
+
|
|
78
106
|
function isPrivateHost(hostname: string): boolean {
|
|
79
107
|
if (hostname === 'localhost' || hostname === '::1') return true;
|
|
80
108
|
|
|
@@ -140,6 +168,7 @@ const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
|
140
168
|
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
141
169
|
|
|
142
170
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
171
|
+
const MANAGED_FILE_NAME_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;
|
|
143
172
|
|
|
144
173
|
class MaxFileSizeExceededError extends Error {
|
|
145
174
|
readonly size: number;
|
|
@@ -241,19 +270,112 @@ export class MediaService {
|
|
|
241
270
|
return encoded;
|
|
242
271
|
}
|
|
243
272
|
|
|
244
|
-
private async
|
|
245
|
-
|
|
273
|
+
private async stageWorkspaceUploadPath(localPath: string): Promise<string> {
|
|
274
|
+
const { savePath, tempPath } = this.buildManagedMediaPath(basename(localPath), 'file');
|
|
246
275
|
|
|
247
276
|
try {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
277
|
+
const fileStat = await stat(localPath);
|
|
278
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
279
|
+
throw new MaxFileSizeExceededError(fileStat.size, MAX_FILE_SIZE);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await copyFile(localPath, tempPath);
|
|
283
|
+
await rename(tempPath, savePath);
|
|
284
|
+
return savePath;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
await unlink(tempPath).catch(() => undefined);
|
|
287
|
+
await unlink(savePath).catch(() => undefined);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async resolveManagedUploadPath(localPath: string): Promise<ResolvedUploadPath | null> {
|
|
293
|
+
await this.ensureDir();
|
|
294
|
+
|
|
295
|
+
const normalizedInputPath = normalizeLocalUploadPath(localPath);
|
|
296
|
+
if (!normalizedInputPath) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const resolvedMediaDir = await realpath(resolveManagedMediaDir()).catch(() => null);
|
|
301
|
+
if (!resolvedMediaDir) {
|
|
255
302
|
return null;
|
|
256
303
|
}
|
|
304
|
+
|
|
305
|
+
const configuredWorkspaceRoots = resolveTrustedWorkspaceDirs();
|
|
306
|
+
const resolvedWorkspaceRoots = (await Promise.all(
|
|
307
|
+
configuredWorkspaceRoots.map(async (workspaceRoot) => {
|
|
308
|
+
try {
|
|
309
|
+
return await realpath(workspaceRoot);
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}),
|
|
314
|
+
)).filter((workspaceRoot): workspaceRoot is string => Boolean(workspaceRoot));
|
|
315
|
+
|
|
316
|
+
const candidatePaths = new Set<string>();
|
|
317
|
+
candidatePaths.add(normalizedInputPath);
|
|
318
|
+
|
|
319
|
+
if (!isAbsolute(normalizedInputPath)) {
|
|
320
|
+
candidatePaths.add(join(resolveManagedMediaDir(), normalizedInputPath));
|
|
321
|
+
for (const workspaceRoot of configuredWorkspaceRoots) {
|
|
322
|
+
candidatePaths.add(resolvePath(workspaceRoot, normalizedInputPath));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (isAbsolute(normalizedInputPath)) {
|
|
327
|
+
const looksLikeWorkspacePath = configuredWorkspaceRoots.some((workspaceRoot) =>
|
|
328
|
+
isPathInside(workspaceRoot, normalizedInputPath),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (looksLikeWorkspacePath) {
|
|
332
|
+
candidatePaths.add(join(resolveManagedMediaDir(), basename(normalizedInputPath)));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const candidatePath of candidatePaths) {
|
|
337
|
+
let filePath: string;
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
filePath = await realpath(candidatePath);
|
|
341
|
+
} catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isPathInside(resolvedMediaDir, filePath)) {
|
|
346
|
+
return { path: filePath, cleanup: false };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const resolvedWorkspaceRoot of resolvedWorkspaceRoots) {
|
|
350
|
+
if (!isPathInside(resolvedWorkspaceRoot, filePath)) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
path: await this.stageWorkspaceUploadPath(filePath),
|
|
356
|
+
cleanup: true,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private resolveOutboundFileName(fileName: string, managedPath: string): string {
|
|
365
|
+
const requestedBaseName = basename(fileName).trim();
|
|
366
|
+
const managedBaseName = basename(managedPath).trim();
|
|
367
|
+
const managedMatch = MANAGED_FILE_NAME_RE.exec(managedBaseName);
|
|
368
|
+
const logicalManagedName = managedMatch?.[1]?.trim() || managedBaseName;
|
|
369
|
+
|
|
370
|
+
if (requestedBaseName && requestedBaseName !== managedBaseName) {
|
|
371
|
+
return requestedBaseName;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (requestedBaseName === managedBaseName && managedMatch?.[1]?.trim()) {
|
|
375
|
+
return managedMatch[1].trim();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return requestedBaseName || logicalManagedName;
|
|
257
379
|
}
|
|
258
380
|
|
|
259
381
|
private async fetchDownloadResponse(params: {
|
|
@@ -425,6 +547,7 @@ export class MediaService {
|
|
|
425
547
|
message?: string;
|
|
426
548
|
}): Promise<UploadedMedia> {
|
|
427
549
|
const { localPath, fileName, webhookUrl, bot, dialogId, message } = params;
|
|
550
|
+
let stagedUploadPath: string | null = null;
|
|
428
551
|
|
|
429
552
|
try {
|
|
430
553
|
const managedPath = await this.resolveManagedUploadPath(localPath);
|
|
@@ -435,9 +558,12 @@ export class MediaService {
|
|
|
435
558
|
});
|
|
436
559
|
return { ok: false };
|
|
437
560
|
}
|
|
561
|
+
if (managedPath.cleanup) {
|
|
562
|
+
stagedUploadPath = managedPath.path;
|
|
563
|
+
}
|
|
438
564
|
|
|
439
565
|
// Check file size before reading
|
|
440
|
-
const fileStat = await stat(managedPath);
|
|
566
|
+
const fileStat = await stat(managedPath.path);
|
|
441
567
|
if (fileStat.size > MAX_FILE_SIZE) {
|
|
442
568
|
this.logger.warn('File too large to upload', {
|
|
443
569
|
fileName,
|
|
@@ -448,10 +574,11 @@ export class MediaService {
|
|
|
448
574
|
}
|
|
449
575
|
|
|
450
576
|
// Encode incrementally to avoid holding both the raw file and base64 string in memory.
|
|
451
|
-
const base64Content = await this.encodeFileToBase64(managedPath);
|
|
577
|
+
const base64Content = await this.encodeFileToBase64(managedPath.path);
|
|
578
|
+
const outboundFileName = this.resolveOutboundFileName(fileName, managedPath.path);
|
|
452
579
|
|
|
453
580
|
const result = await this.api.uploadFile(webhookUrl, bot, dialogId, {
|
|
454
|
-
name:
|
|
581
|
+
name: outboundFileName,
|
|
455
582
|
content: base64Content,
|
|
456
583
|
message,
|
|
457
584
|
});
|
|
@@ -469,6 +596,10 @@ export class MediaService {
|
|
|
469
596
|
error: serializeError(err),
|
|
470
597
|
});
|
|
471
598
|
return { ok: false };
|
|
599
|
+
} finally {
|
|
600
|
+
if (stagedUploadPath) {
|
|
601
|
+
await unlink(stagedUploadPath).catch(() => undefined);
|
|
602
|
+
}
|
|
472
603
|
}
|
|
473
604
|
}
|
|
474
605
|
|
package/src/message-utils.ts
CHANGED
|
@@ -149,6 +149,7 @@ export function markdownToBbCode(md: string): string {
|
|
|
149
149
|
text = text.replace(/~~(.+?)~~/g, '[S]$1[/S]');
|
|
150
150
|
|
|
151
151
|
// 3f. HTML inline formatting tags
|
|
152
|
+
text = text.replace(/<br\s*\/?>/gi, '[BR]');
|
|
152
153
|
text = text.replace(/<u>([\s\S]*?)<\/u>/gi, '[U]$1[/U]');
|
|
153
154
|
text = text.replace(/<b>([\s\S]*?)<\/b>/gi, '[B]$1[/B]');
|
|
154
155
|
text = text.replace(/<strong>([\s\S]*?)<\/strong>/gi, '[B]$1[/B]');
|
|
@@ -158,13 +159,18 @@ export function markdownToBbCode(md: string): string {
|
|
|
158
159
|
text = text.replace(/<del>([\s\S]*?)<\/del>/gi, '[S]$1[/S]');
|
|
159
160
|
text = text.replace(/<strike>([\s\S]*?)<\/strike>/gi, '[S]$1[/S]');
|
|
160
161
|
|
|
161
|
-
// 3g. Links: [text](url) →
|
|
162
|
-
text = text.replace(/\[([^\]]+)\]\(([
|
|
162
|
+
// 3g. Links: [text](url) → Bitrix BB-code links and action links
|
|
163
|
+
text = text.replace(/\[([^\]]+)\]\(([\s\S]*?)\)/g, (_match, label: string, rawTarget: string) => {
|
|
164
|
+
return convertMarkdownLink(label, rawTarget);
|
|
165
|
+
});
|
|
163
166
|
|
|
164
167
|
// 3h. Autolink URL: <https://...> → [URL]https://...[/URL]
|
|
165
168
|
text = text.replace(/<(https?:\/\/[^>]+)>/g, '[URL]$1[/URL]');
|
|
166
169
|
|
|
167
|
-
// 3i. Autolink
|
|
170
|
+
// 3i. Autolink tel URI: <tel:+79991234567> → [CALL]+79991234567[/CALL]
|
|
171
|
+
text = text.replace(/<tel:([^>]+)>/gi, '[CALL]$1[/CALL]');
|
|
172
|
+
|
|
173
|
+
// 3j. Autolink email: <user@example.com> → [URL]mailto:user@example.com[/URL]
|
|
168
174
|
text = text.replace(/<([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})>/g, '[URL]mailto:$1[/URL]');
|
|
169
175
|
|
|
170
176
|
// ── Phase 4: Restore placeholders ─────────────────────────────────────────
|
|
@@ -645,6 +651,87 @@ function extractMarkdownTarget(rawTarget: string): string {
|
|
|
645
651
|
return trimmed;
|
|
646
652
|
}
|
|
647
653
|
|
|
654
|
+
function convertMarkdownLink(label: string, rawTarget: string): string {
|
|
655
|
+
const target = extractMarkdownTarget(rawTarget);
|
|
656
|
+
const normalizedTarget = target.trim();
|
|
657
|
+
|
|
658
|
+
if (!normalizedTarget) {
|
|
659
|
+
return label;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const sendMatch = normalizedTarget.match(/^send:(.+)$/i);
|
|
663
|
+
if (sendMatch) {
|
|
664
|
+
const payload = normalizeActionLinkPayload(sendMatch[1]);
|
|
665
|
+
return payload ? `[SEND=${payload}]${label}[/SEND]` : `[SEND]${label}[/SEND]`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const putMatch = normalizedTarget.match(/^put:(.+)$/i);
|
|
669
|
+
if (putMatch) {
|
|
670
|
+
const payload = normalizeActionLinkPayload(putMatch[1]);
|
|
671
|
+
return payload ? `[PUT=${payload}]${label}[/PUT]` : `[PUT]${label}[/PUT]`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const callMatch = normalizedTarget.match(/^(?:call|tel):(.+)$/i);
|
|
675
|
+
if (callMatch) {
|
|
676
|
+
const phone = callMatch[1].trim();
|
|
677
|
+
return phone ? `[CALL=${phone}]${label}[/CALL]` : `[CALL]${label}[/CALL]`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const userMatch = normalizedTarget.match(/^user:(.+)$/i);
|
|
681
|
+
if (userMatch) {
|
|
682
|
+
return `[USER=${userMatch[1].trim()}]${label}[/USER]`;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const chatMatch = normalizedTarget.match(/^chat:(.+)$/i);
|
|
686
|
+
if (chatMatch) {
|
|
687
|
+
return `[CHAT=${chatMatch[1].trim()}]${label}[/CHAT]`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const contextMatch = normalizedTarget.match(/^context:(.+)$/i);
|
|
691
|
+
if (contextMatch) {
|
|
692
|
+
return `[context=${contextMatch[1].trim()}]${label}[/context]`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const diskMatch = normalizedTarget.match(/^disk:(.+)$/i);
|
|
696
|
+
if (diskMatch) {
|
|
697
|
+
return `[disk=${diskMatch[1].trim()}]`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const timestampMatch = normalizedTarget.match(/^timestamp:(.+)$/i);
|
|
701
|
+
if (timestampMatch) {
|
|
702
|
+
const timestampMarkup = buildTimestampBbCode(timestampMatch[1].trim());
|
|
703
|
+
if (timestampMarkup) {
|
|
704
|
+
return timestampMarkup;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return `[URL=${normalizedTarget}]${label}[/URL]`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function normalizeActionLinkPayload(value: string): string {
|
|
712
|
+
return value.trim().replace(/^\/\//, '/');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function buildTimestampBbCode(value: string): string {
|
|
716
|
+
if (!value) {
|
|
717
|
+
return '';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const [timestamp, rawQuery = ''] = value.split('?', 2);
|
|
721
|
+
const normalizedTimestamp = timestamp.trim();
|
|
722
|
+
if (!normalizedTimestamp) {
|
|
723
|
+
return '';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const params = new URLSearchParams(rawQuery);
|
|
727
|
+
const format = params.get('format')?.trim();
|
|
728
|
+
if (format) {
|
|
729
|
+
return `[timestamp=${normalizedTimestamp} format=${format}]`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return `[timestamp=${normalizedTimestamp}]`;
|
|
733
|
+
}
|
|
734
|
+
|
|
648
735
|
function normalizeBitrixImageUrl(rawUrl: string): string {
|
|
649
736
|
const trimmed = rawUrl.trim();
|
|
650
737
|
if (!trimmed) return trimmed;
|
package/src/runtime.ts
CHANGED
package/src/send-service.ts
CHANGED
|
@@ -41,6 +41,7 @@ export class SendService {
|
|
|
41
41
|
options?: {
|
|
42
42
|
keyboard?: B24Keyboard;
|
|
43
43
|
convertMarkdown?: boolean;
|
|
44
|
+
replyToMessageId?: number;
|
|
44
45
|
forwardMessages?: number[];
|
|
45
46
|
system?: boolean;
|
|
46
47
|
},
|
|
@@ -49,8 +50,41 @@ export class SendService {
|
|
|
49
50
|
? markdownToBbCode(text)
|
|
50
51
|
: text;
|
|
51
52
|
|
|
52
|
-
const chunks = splitMessage(convertedText);
|
|
53
|
-
if (chunks.length === 0)
|
|
53
|
+
const chunks = convertedText.length > 0 ? splitMessage(convertedText) : [];
|
|
54
|
+
if (chunks.length === 0) {
|
|
55
|
+
if (!options?.forwardMessages?.length) {
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const msgOptions: {
|
|
60
|
+
forwardMessages?: number[];
|
|
61
|
+
replyToMessageId?: number;
|
|
62
|
+
system?: boolean;
|
|
63
|
+
} = {
|
|
64
|
+
forwardMessages: options.forwardMessages,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (options?.replyToMessageId) {
|
|
68
|
+
msgOptions.replyToMessageId = options.replyToMessageId;
|
|
69
|
+
}
|
|
70
|
+
if (options?.system !== undefined) {
|
|
71
|
+
msgOptions.system = options.system;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const messageId = await this.api.sendMessage(
|
|
76
|
+
ctx.webhookUrl,
|
|
77
|
+
ctx.bot,
|
|
78
|
+
ctx.dialogId,
|
|
79
|
+
null,
|
|
80
|
+
msgOptions,
|
|
81
|
+
);
|
|
82
|
+
return { ok: true, messageId };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.logger.error('Failed to send forwarded message without text', { error: serializeError(error) });
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
54
88
|
let lastMessageId: number | undefined;
|
|
55
89
|
|
|
56
90
|
for (let i = 0; i < chunks.length; i++) {
|
|
@@ -58,6 +92,7 @@ export class SendService {
|
|
|
58
92
|
const isLast = i === chunks.length - 1;
|
|
59
93
|
const msgOptions: {
|
|
60
94
|
keyboard?: B24Keyboard;
|
|
95
|
+
replyToMessageId?: number;
|
|
61
96
|
forwardMessages?: number[];
|
|
62
97
|
system?: boolean;
|
|
63
98
|
} = {};
|
|
@@ -65,6 +100,9 @@ export class SendService {
|
|
|
65
100
|
if (isLast && options?.keyboard) {
|
|
66
101
|
msgOptions.keyboard = options.keyboard;
|
|
67
102
|
}
|
|
103
|
+
if (isFirst && options?.replyToMessageId) {
|
|
104
|
+
msgOptions.replyToMessageId = options.replyToMessageId;
|
|
105
|
+
}
|
|
68
106
|
if (isFirst && options?.forwardMessages?.length) {
|
|
69
107
|
msgOptions.forwardMessages = options.forwardMessages;
|
|
70
108
|
}
|
package/src/state-paths.ts
CHANGED
|
@@ -23,6 +23,17 @@ export function resolveManagedMediaDir(env: NodeJS.ProcessEnv = process.env): st
|
|
|
23
23
|
return join(resolveOpenClawStateDir(env), 'media', 'bitrix24');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function resolveTrustedWorkspaceDirs(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
27
|
+
const roots = [
|
|
28
|
+
env.OPENCLAW_WORKSPACE_DIR?.trim(),
|
|
29
|
+
env.CLAWDBOT_WORKSPACE_DIR?.trim(),
|
|
30
|
+
join(resolveOpenClawStateDir(env), 'workspace'),
|
|
31
|
+
join(resolveHomeDir(env), 'workspace'),
|
|
32
|
+
].filter((value): value is string => Boolean(value));
|
|
33
|
+
|
|
34
|
+
return [...new Set(roots.map((root) => resolvePath(root)))];
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export function resolvePollingStateDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
27
38
|
return join(resolveOpenClawStateDir(env), 'state', 'bitrix24');
|
|
28
39
|
}
|
package/src/types.ts
CHANGED
|
@@ -346,12 +346,134 @@ export interface KeyboardNewline {
|
|
|
346
346
|
/** B24 keyboard: flat array with NEWLINE separators between rows */
|
|
347
347
|
export type B24Keyboard = (KeyboardButton | KeyboardNewline)[];
|
|
348
348
|
|
|
349
|
+
export type B24AttachColorToken = 'primary' | 'secondary' | 'alert' | 'base';
|
|
350
|
+
|
|
351
|
+
export interface B24AttachMessageBlock {
|
|
352
|
+
MESSAGE: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface B24AttachLinkValue {
|
|
356
|
+
LINK: string;
|
|
357
|
+
NAME?: string;
|
|
358
|
+
DESC?: string;
|
|
359
|
+
HTML?: string;
|
|
360
|
+
PREVIEW?: string;
|
|
361
|
+
WIDTH?: number;
|
|
362
|
+
HEIGHT?: number;
|
|
363
|
+
USER_ID?: number;
|
|
364
|
+
CHAT_ID?: number;
|
|
365
|
+
NETWORK_ID?: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface B24AttachLinkBlock {
|
|
369
|
+
LINK: B24AttachLinkValue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface B24AttachImageItem {
|
|
373
|
+
LINK: string;
|
|
374
|
+
NAME?: string;
|
|
375
|
+
PREVIEW?: string;
|
|
376
|
+
WIDTH?: number;
|
|
377
|
+
HEIGHT?: number;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface B24AttachImageBlock {
|
|
381
|
+
IMAGE: B24AttachImageItem | B24AttachImageItem[];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export interface B24AttachFileItem {
|
|
385
|
+
LINK: string;
|
|
386
|
+
NAME?: string;
|
|
387
|
+
SIZE?: number;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface B24AttachFileBlock {
|
|
391
|
+
FILE: B24AttachFileItem | B24AttachFileItem[];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export interface B24AttachDelimiterValue {
|
|
395
|
+
SIZE?: number;
|
|
396
|
+
COLOR?: string;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export interface B24AttachDelimiterBlock {
|
|
400
|
+
DELIMITER: B24AttachDelimiterValue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export type B24AttachGridDisplay = 'BLOCK' | 'LINE' | 'ROW' | 'TABLE';
|
|
404
|
+
|
|
405
|
+
export interface B24AttachGridItem {
|
|
406
|
+
DISPLAY: B24AttachGridDisplay;
|
|
407
|
+
NAME?: string;
|
|
408
|
+
VALUE?: string;
|
|
409
|
+
WIDTH?: number;
|
|
410
|
+
HEIGHT?: number;
|
|
411
|
+
COLOR_TOKEN?: B24AttachColorToken;
|
|
412
|
+
COLOR?: string;
|
|
413
|
+
LINK?: string;
|
|
414
|
+
USER_ID?: number;
|
|
415
|
+
CHAT_ID?: number;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export interface B24AttachGridBlock {
|
|
419
|
+
GRID: B24AttachGridItem[];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export interface B24AttachUserValue {
|
|
423
|
+
NAME?: string;
|
|
424
|
+
AVATAR?: string;
|
|
425
|
+
LINK?: string;
|
|
426
|
+
USER_ID?: number;
|
|
427
|
+
NETWORK_ID?: string;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export interface B24AttachUserBlock {
|
|
431
|
+
USER: B24AttachUserValue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export type B24AttachBlock =
|
|
435
|
+
| B24AttachMessageBlock
|
|
436
|
+
| B24AttachLinkBlock
|
|
437
|
+
| B24AttachImageBlock
|
|
438
|
+
| B24AttachFileBlock
|
|
439
|
+
| B24AttachDelimiterBlock
|
|
440
|
+
| B24AttachGridBlock
|
|
441
|
+
| B24AttachUserBlock;
|
|
442
|
+
|
|
443
|
+
export interface B24AttachEnvelope {
|
|
444
|
+
ID?: number;
|
|
445
|
+
COLOR_TOKEN?: B24AttachColorToken;
|
|
446
|
+
COLOR?: string;
|
|
447
|
+
BLOCKS: B24AttachBlock[];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export type B24Attach = B24AttachEnvelope | B24AttachBlock[];
|
|
451
|
+
|
|
349
452
|
export interface SendMessageResult {
|
|
350
453
|
ok: boolean;
|
|
351
454
|
messageId?: number;
|
|
352
455
|
error?: string;
|
|
353
456
|
}
|
|
354
457
|
|
|
458
|
+
// ─── Message Action Discovery ────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
export interface ChannelMessageToolSchemaContribution {
|
|
461
|
+
properties: Record<string, unknown>;
|
|
462
|
+
visibility?: 'current-channel' | 'all-configured';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export interface ChannelMessageToolDiscovery {
|
|
466
|
+
actions: string[];
|
|
467
|
+
capabilities: string[];
|
|
468
|
+
schema: ChannelMessageToolSchemaContribution | ChannelMessageToolSchemaContribution[] | null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export interface ExtractedToolSendTarget {
|
|
472
|
+
to: string;
|
|
473
|
+
accountId?: string;
|
|
474
|
+
threadId?: string;
|
|
475
|
+
}
|
|
476
|
+
|
|
355
477
|
// ─── FETCH Mode Context ─────────────────────────────────────────────────────
|
|
356
478
|
|
|
357
479
|
/** Context passed from PollingService to InboundHandler for FETCH events */
|