bitrix24-tasks-mcp-server 1.1.0 → 1.4.0
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 +55 -2
- package/build/bitrix24/client.d.ts +123 -0
- package/build/bitrix24/client.d.ts.map +1 -1
- package/build/bitrix24/client.js +697 -7
- package/build/bitrix24/client.js.map +1 -1
- package/build/bitrix24/taskChat.d.ts +36 -0
- package/build/bitrix24/taskChat.d.ts.map +1 -0
- package/build/bitrix24/taskChat.js +108 -0
- package/build/bitrix24/taskChat.js.map +1 -0
- package/build/bitrix24/taskComments.d.ts +21 -0
- package/build/bitrix24/taskComments.d.ts.map +1 -0
- package/build/bitrix24/taskComments.js +67 -0
- package/build/bitrix24/taskComments.js.map +1 -0
- package/build/bitrix24/taskFiles.d.ts +9 -1
- package/build/bitrix24/taskFiles.d.ts.map +1 -1
- package/build/bitrix24/taskFiles.js +79 -3
- package/build/bitrix24/taskFiles.js.map +1 -1
- package/build/index.js +23 -8
- package/build/index.js.map +1 -1
- package/build/tools/index.d.ts +3 -0
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +235 -0
- package/build/tools/index.js.map +1 -1
- package/package.json +2 -2
package/build/bitrix24/client.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { readLocalFileAsBase64, toWebdavFileToken } from './taskFiles.js';
|
|
3
|
+
import { collectDiskFileIdsFromTask, guessMimeType, isImageFile, parseWebdavFileToken, readLocalFileAsBase64, saveBufferToDirectory, toWebdavFileToken } from './taskFiles.js';
|
|
4
|
+
import { extractCommentAttachments, normalizeTaskComment } from './taskComments.js';
|
|
5
|
+
import { buildDialogId, indexChatFiles, isImageChatFile, normalizeChatFile, normalizeChatMessage, resolveMessageAttachments } from './taskChat.js';
|
|
4
6
|
const BITRIX24_WEBHOOK_URL = process.env.BITRIX24_WEBHOOK_URL ||
|
|
5
7
|
'https://sviluppofranchising.bitrix24.it/rest/27/wwugdez6m774803q/';
|
|
6
8
|
const BitrixResponseSchema = z.object({
|
|
@@ -24,6 +26,39 @@ export class Bitrix24Client {
|
|
|
24
26
|
constructor(webhookUrl = BITRIX24_WEBHOOK_URL) {
|
|
25
27
|
this.baseUrl = webhookUrl.replace(/\/$/, '');
|
|
26
28
|
}
|
|
29
|
+
getPortalOrigin() {
|
|
30
|
+
const match = this.baseUrl.match(/^(https?:\/\/[^/]+)/i);
|
|
31
|
+
return match?.[1] ?? '';
|
|
32
|
+
}
|
|
33
|
+
buildMethodUrl(method, useApiV3 = false) {
|
|
34
|
+
let base = this.baseUrl;
|
|
35
|
+
if (useApiV3) {
|
|
36
|
+
if (!/\/rest\/api\//i.test(base)) {
|
|
37
|
+
base = base.replace(/\/rest\//i, '/rest/api/');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
base = base.replace(/\/rest\/api\//i, '/rest/');
|
|
42
|
+
}
|
|
43
|
+
return `${base}/${method}`;
|
|
44
|
+
}
|
|
45
|
+
extractApiError(data) {
|
|
46
|
+
const error = data.error;
|
|
47
|
+
if (!error) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (typeof error === 'string') {
|
|
51
|
+
const description = data.error_description;
|
|
52
|
+
return description ? `${error} - ${String(description)}` : error;
|
|
53
|
+
}
|
|
54
|
+
if (typeof error === 'object' && error !== null) {
|
|
55
|
+
const record = error;
|
|
56
|
+
const code = record.code ?? record.error;
|
|
57
|
+
const message = record.message ?? record.error_description;
|
|
58
|
+
return `${String(code ?? 'ERROR')}: ${String(message ?? 'Unknown error')}`;
|
|
59
|
+
}
|
|
60
|
+
return 'Unknown Bitrix24 API error';
|
|
61
|
+
}
|
|
27
62
|
async enforceRateLimit() {
|
|
28
63
|
const now = Date.now();
|
|
29
64
|
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
@@ -32,9 +67,12 @@ export class Bitrix24Client {
|
|
|
32
67
|
}
|
|
33
68
|
this.lastRequestTime = Date.now();
|
|
34
69
|
}
|
|
35
|
-
async
|
|
70
|
+
async callRest(method, params = {}, options = {}) {
|
|
71
|
+
return this.makeRequest(method, params, options);
|
|
72
|
+
}
|
|
73
|
+
async makeRequest(method, params = {}, options = {}) {
|
|
36
74
|
await this.enforceRateLimit();
|
|
37
|
-
const url =
|
|
75
|
+
const url = this.buildMethodUrl(method, options.useApiV3 === true);
|
|
38
76
|
try {
|
|
39
77
|
let response;
|
|
40
78
|
if (Object.keys(params).length === 0) {
|
|
@@ -64,11 +102,15 @@ export class Bitrix24Client {
|
|
|
64
102
|
console.error(`HTTP Error ${response.status}:`, errorText);
|
|
65
103
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
66
104
|
}
|
|
67
|
-
const data = await response.json();
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
throw new Error(`Bitrix24 API Error: ${
|
|
105
|
+
const data = (await response.json());
|
|
106
|
+
const apiError = this.extractApiError(data);
|
|
107
|
+
if (apiError) {
|
|
108
|
+
throw new Error(`Bitrix24 API Error: ${apiError}`);
|
|
71
109
|
}
|
|
110
|
+
if (!('result' in data)) {
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
const parsed = BitrixResponseSchema.parse(data);
|
|
72
114
|
return parsed.result;
|
|
73
115
|
}
|
|
74
116
|
catch (error) {
|
|
@@ -387,6 +429,631 @@ export class Bitrix24Client {
|
|
|
387
429
|
}
|
|
388
430
|
return { attached, errors };
|
|
389
431
|
}
|
|
432
|
+
normalizeTaskAttachedFile(raw) {
|
|
433
|
+
return {
|
|
434
|
+
attachmentId: String(raw.ATTACHMENT_ID ?? raw.attachmentId ?? ''),
|
|
435
|
+
fileId: raw.FILE_ID !== undefined ? String(raw.FILE_ID) : raw.fileId !== undefined ? String(raw.fileId) : undefined,
|
|
436
|
+
name: String(raw.NAME ?? raw.name ?? 'file'),
|
|
437
|
+
size: raw.SIZE !== undefined ? Number(raw.SIZE) : raw.size !== undefined ? Number(raw.size) : undefined,
|
|
438
|
+
downloadUrl: raw.DOWNLOAD_URL !== undefined
|
|
439
|
+
? String(raw.DOWNLOAD_URL)
|
|
440
|
+
: raw.downloadUrl !== undefined
|
|
441
|
+
? String(raw.downloadUrl)
|
|
442
|
+
: undefined,
|
|
443
|
+
viewUrl: raw.VIEW_URL !== undefined
|
|
444
|
+
? String(raw.VIEW_URL)
|
|
445
|
+
: raw.viewUrl !== undefined
|
|
446
|
+
? String(raw.viewUrl)
|
|
447
|
+
: undefined
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async listTaskAttachedFiles(taskId) {
|
|
451
|
+
const result = await this.makeRequest('task.item.getfiles', { TASKID: Number(taskId) });
|
|
452
|
+
if (!Array.isArray(result)) {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
return result
|
|
456
|
+
.map((item) => this.normalizeTaskAttachedFile(item))
|
|
457
|
+
.filter((item) => item.attachmentId);
|
|
458
|
+
}
|
|
459
|
+
matchesRequestedFileId(file, requestedId) {
|
|
460
|
+
const normalized = requestedId.replace(/^n/i, '').trim();
|
|
461
|
+
if (!normalized) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
return (file.attachmentId === normalized ||
|
|
465
|
+
file.fileId === normalized ||
|
|
466
|
+
file.fileId === requestedId.replace(/^n/i, '') ||
|
|
467
|
+
(requestedId.startsWith('n') && file.fileId === normalized));
|
|
468
|
+
}
|
|
469
|
+
selectTaskAttachedFiles(attachments, requestedIds) {
|
|
470
|
+
if (requestedIds.length === 0) {
|
|
471
|
+
return attachments;
|
|
472
|
+
}
|
|
473
|
+
const selected = [];
|
|
474
|
+
for (const requestedId of requestedIds) {
|
|
475
|
+
const match = attachments.find((file) => this.matchesRequestedFileId(file, requestedId));
|
|
476
|
+
if (match && !selected.some((item) => item.attachmentId === match.attachmentId)) {
|
|
477
|
+
selected.push(match);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return selected;
|
|
481
|
+
}
|
|
482
|
+
async getDiskFile(diskFileId) {
|
|
483
|
+
const result = await this.makeRequest('disk.file.get', { id: diskFileId });
|
|
484
|
+
const name = String(result?.NAME ?? result?.name ?? `file-${diskFileId}`);
|
|
485
|
+
const downloadUrl = result?.DOWNLOAD_URL ?? result?.downloadUrl;
|
|
486
|
+
const detailUrl = result?.DETAIL_URL ?? result?.detailUrl;
|
|
487
|
+
const sizeRaw = result?.SIZE ?? result?.size;
|
|
488
|
+
return {
|
|
489
|
+
id: String(result?.ID ?? result?.id ?? diskFileId),
|
|
490
|
+
name,
|
|
491
|
+
size: sizeRaw !== undefined ? Number(sizeRaw) : undefined,
|
|
492
|
+
downloadUrl: downloadUrl ? String(downloadUrl) : undefined,
|
|
493
|
+
detailUrl: detailUrl ? String(detailUrl) : undefined,
|
|
494
|
+
mimeType: guessMimeType(name),
|
|
495
|
+
isImage: isImageFile(name)
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
resolveDownloadUrl(downloadUrl) {
|
|
499
|
+
if (/^https?:\/\//i.test(downloadUrl)) {
|
|
500
|
+
return downloadUrl;
|
|
501
|
+
}
|
|
502
|
+
const origin = this.getPortalOrigin();
|
|
503
|
+
if (!origin) {
|
|
504
|
+
return downloadUrl;
|
|
505
|
+
}
|
|
506
|
+
return downloadUrl.startsWith('/') ? `${origin}${downloadUrl}` : `${origin}/${downloadUrl}`;
|
|
507
|
+
}
|
|
508
|
+
async downloadDiskFileContent(downloadUrl) {
|
|
509
|
+
const resolvedUrl = this.resolveDownloadUrl(downloadUrl);
|
|
510
|
+
await this.enforceRateLimit();
|
|
511
|
+
const response = await fetch(resolvedUrl, {
|
|
512
|
+
method: 'GET',
|
|
513
|
+
headers: { Accept: '*/*' }
|
|
514
|
+
});
|
|
515
|
+
if (!response.ok) {
|
|
516
|
+
const errorText = await response.text();
|
|
517
|
+
throw new Error(`Failed to download file (${response.status}): ${errorText.slice(0, 200)}`);
|
|
518
|
+
}
|
|
519
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
520
|
+
return Buffer.from(arrayBuffer);
|
|
521
|
+
}
|
|
522
|
+
collectTaskDiskFileIds(task) {
|
|
523
|
+
return collectDiskFileIdsFromTask(task);
|
|
524
|
+
}
|
|
525
|
+
async downloadTaskAttachedFile(attached, options) {
|
|
526
|
+
const includeContent = options.includeContent !== false;
|
|
527
|
+
const imagesOnly = options.imagesOnly === true;
|
|
528
|
+
const isImage = isImageFile(attached.name);
|
|
529
|
+
const diskFileId = attached.fileId ? toWebdavFileToken(attached.fileId) : attached.attachmentId;
|
|
530
|
+
const entry = {
|
|
531
|
+
diskFileId,
|
|
532
|
+
attachmentId: attached.attachmentId,
|
|
533
|
+
fileId: attached.fileId,
|
|
534
|
+
fileName: attached.name,
|
|
535
|
+
mimeType: guessMimeType(attached.name),
|
|
536
|
+
size: attached.size,
|
|
537
|
+
isImage,
|
|
538
|
+
downloadUrl: attached.downloadUrl,
|
|
539
|
+
viewUrl: attached.viewUrl
|
|
540
|
+
};
|
|
541
|
+
const shouldDownload = includeContent && attached.downloadUrl && (!imagesOnly || isImage);
|
|
542
|
+
if (shouldDownload && attached.downloadUrl) {
|
|
543
|
+
const buffer = await this.downloadDiskFileContent(attached.downloadUrl);
|
|
544
|
+
if (options.saveToDirectory) {
|
|
545
|
+
entry.savedPath = await saveBufferToDirectory(options.saveToDirectory, attached.name, buffer);
|
|
546
|
+
}
|
|
547
|
+
entry.base64 = buffer.toString('base64');
|
|
548
|
+
}
|
|
549
|
+
return entry;
|
|
550
|
+
}
|
|
551
|
+
async getTaskFiles(taskId, options = {}) {
|
|
552
|
+
const explicitIds = options.diskFileIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
553
|
+
let discoveredIds = [];
|
|
554
|
+
if (explicitIds.length === 0) {
|
|
555
|
+
const task = await this.getTask(taskId);
|
|
556
|
+
discoveredIds = this.collectTaskDiskFileIds(task);
|
|
557
|
+
}
|
|
558
|
+
const requestedIds = explicitIds.length > 0 ? explicitIds : discoveredIds;
|
|
559
|
+
const uniqueIds = [...new Set(requestedIds.filter(Boolean))];
|
|
560
|
+
const attachments = await this.listTaskAttachedFiles(taskId);
|
|
561
|
+
const selected = uniqueIds.length === 0
|
|
562
|
+
? attachments
|
|
563
|
+
: this.selectTaskAttachedFiles(attachments, uniqueIds);
|
|
564
|
+
const files = [];
|
|
565
|
+
const errors = [];
|
|
566
|
+
for (const attached of selected) {
|
|
567
|
+
try {
|
|
568
|
+
const isImage = isImageFile(attached.name);
|
|
569
|
+
if (options.imagesOnly === true && !isImage) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
files.push(await this.downloadTaskAttachedFile(attached, options));
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
576
|
+
const refId = attached.fileId ?? attached.attachmentId;
|
|
577
|
+
errors.push(`${refId}: ${message}`);
|
|
578
|
+
files.push({
|
|
579
|
+
diskFileId: attached.fileId ? toWebdavFileToken(attached.fileId) : attached.attachmentId,
|
|
580
|
+
attachmentId: attached.attachmentId,
|
|
581
|
+
fileId: attached.fileId,
|
|
582
|
+
fileName: attached.name,
|
|
583
|
+
mimeType: guessMimeType(attached.name),
|
|
584
|
+
isImage: isImageFile(attached.name),
|
|
585
|
+
error: message
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const unresolvedIds = uniqueIds.filter((id) => !attachments.some((file) => this.matchesRequestedFileId(file, id)) &&
|
|
590
|
+
!files.some((file) => file.diskFileId === id || file.attachmentId === id || file.fileId === id));
|
|
591
|
+
for (const unresolvedId of unresolvedIds) {
|
|
592
|
+
try {
|
|
593
|
+
const diskFile = await this.getDiskFile(parseWebdavFileToken(unresolvedId));
|
|
594
|
+
const attachedLike = {
|
|
595
|
+
attachmentId: unresolvedId,
|
|
596
|
+
fileId: diskFile.id,
|
|
597
|
+
name: diskFile.name,
|
|
598
|
+
size: diskFile.size,
|
|
599
|
+
downloadUrl: diskFile.downloadUrl,
|
|
600
|
+
viewUrl: diskFile.detailUrl
|
|
601
|
+
};
|
|
602
|
+
if (options.imagesOnly === true && !diskFile.isImage) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
files.push(await this.downloadTaskAttachedFile(attachedLike, options));
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
609
|
+
errors.push(`${unresolvedId}: ${message}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { taskId, diskFileIds: uniqueIds, files, errors };
|
|
613
|
+
}
|
|
614
|
+
async resolveTaskChat(taskId) {
|
|
615
|
+
try {
|
|
616
|
+
const legacy = await this.makeRequest('tasks.task.get', {
|
|
617
|
+
taskId,
|
|
618
|
+
select: ['ID', 'CHAT_ID']
|
|
619
|
+
});
|
|
620
|
+
const chatId = legacy?.task?.CHAT_ID ?? legacy?.task?.chatId;
|
|
621
|
+
if (chatId !== undefined && chatId !== null && chatId !== '') {
|
|
622
|
+
return { chatId: String(chatId), dialogId: buildDialogId(chatId) };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
console.error(`tasks.task.get CHAT_ID failed for task ${taskId}:`, error);
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const modern = await this.makeRequest('tasks.task.get', {
|
|
630
|
+
id: Number(taskId),
|
|
631
|
+
select: ['id', 'chat.id']
|
|
632
|
+
}, { useApiV3: true });
|
|
633
|
+
const chatId = modern?.item?.chat?.id ?? modern?.chat?.id;
|
|
634
|
+
if (chatId !== undefined && chatId !== null && chatId !== '') {
|
|
635
|
+
return { chatId: String(chatId), dialogId: buildDialogId(chatId) };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
console.error(`tasks.task.get (v3) chat.id failed for task ${taskId}:`, error);
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
async listTaskComments(taskId, options = {}) {
|
|
644
|
+
const payload = {
|
|
645
|
+
TASKID: Number(taskId)
|
|
646
|
+
};
|
|
647
|
+
if (options.order && Object.keys(options.order).length > 0) {
|
|
648
|
+
payload.ORDER = options.order;
|
|
649
|
+
}
|
|
650
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
651
|
+
payload.FILTER = options.filter;
|
|
652
|
+
}
|
|
653
|
+
const result = await this.makeRequest('task.commentitem.getlist', payload);
|
|
654
|
+
if (!Array.isArray(result)) {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
return result
|
|
658
|
+
.map((item) => normalizeTaskComment(item))
|
|
659
|
+
.filter((comment) => comment.id);
|
|
660
|
+
}
|
|
661
|
+
async getTaskComment(taskId, commentId) {
|
|
662
|
+
const result = await this.makeRequest('task.commentitem.get', {
|
|
663
|
+
TASKID: Number(taskId),
|
|
664
|
+
ITEMID: Number(commentId)
|
|
665
|
+
});
|
|
666
|
+
if (!result || typeof result !== 'object') {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
const comment = normalizeTaskComment(result);
|
|
670
|
+
return comment.id ? comment : null;
|
|
671
|
+
}
|
|
672
|
+
async fetchTaskCommentRawItems(taskId, options) {
|
|
673
|
+
const payload = {
|
|
674
|
+
TASKID: Number(taskId)
|
|
675
|
+
};
|
|
676
|
+
if (options.order && Object.keys(options.order).length > 0) {
|
|
677
|
+
payload.ORDER = options.order;
|
|
678
|
+
}
|
|
679
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
680
|
+
payload.FILTER = options.filter;
|
|
681
|
+
}
|
|
682
|
+
const result = await this.makeRequest('task.commentitem.getlist', payload);
|
|
683
|
+
if (!Array.isArray(result)) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
async downloadChatFileAttachment(commentId, file, options) {
|
|
689
|
+
const attached = {
|
|
690
|
+
attachmentId: file.attachmentId,
|
|
691
|
+
fileId: file.fileId,
|
|
692
|
+
name: file.name,
|
|
693
|
+
size: file.size,
|
|
694
|
+
downloadUrl: file.downloadUrl,
|
|
695
|
+
viewUrl: file.viewUrl
|
|
696
|
+
};
|
|
697
|
+
const downloaded = await this.downloadTaskAttachedFile(attached, options);
|
|
698
|
+
return { ...downloaded, commentId };
|
|
699
|
+
}
|
|
700
|
+
async getTaskCommentsFromChat(taskId, chat, options) {
|
|
701
|
+
const payload = {
|
|
702
|
+
DIALOG_ID: chat.dialogId,
|
|
703
|
+
LIMIT: Math.min(Math.max(options.limit ?? 50, 1), 50)
|
|
704
|
+
};
|
|
705
|
+
if (options.lastId !== undefined) {
|
|
706
|
+
payload.LAST_ID = options.lastId;
|
|
707
|
+
}
|
|
708
|
+
if (options.firstId !== undefined) {
|
|
709
|
+
payload.FIRST_ID = options.firstId;
|
|
710
|
+
}
|
|
711
|
+
const result = await this.makeRequest('im.dialog.messages.get', payload);
|
|
712
|
+
const messagesRaw = Array.isArray(result?.messages) ? result.messages : [];
|
|
713
|
+
const filesRaw = Array.isArray(result?.files) ? result.files : [];
|
|
714
|
+
const files = filesRaw
|
|
715
|
+
.map((item) => normalizeChatFile(item))
|
|
716
|
+
.filter((item) => item !== null);
|
|
717
|
+
const filesById = indexChatFiles(files);
|
|
718
|
+
const includeAttachments = options.includeAttachments !== false;
|
|
719
|
+
const requestedCommentIds = options.commentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
720
|
+
const requestedAttachmentIds = options.attachmentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
721
|
+
const comments = [];
|
|
722
|
+
const errors = [];
|
|
723
|
+
for (const rawMessage of messagesRaw) {
|
|
724
|
+
const message = normalizeChatMessage(rawMessage);
|
|
725
|
+
if (!message) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (!options.includeSystemMessages && message.isSystem) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (requestedCommentIds.length > 0 && !requestedCommentIds.includes(message.id)) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const attachments = resolveMessageAttachments(message, filesById);
|
|
735
|
+
const selectedAttachments = this.selectCommentAttachments(attachments, requestedAttachmentIds);
|
|
736
|
+
const downloadedFiles = [];
|
|
737
|
+
if (includeAttachments) {
|
|
738
|
+
for (const attachment of selectedAttachments) {
|
|
739
|
+
const chatFile = filesById.get(attachment.fileId ?? attachment.attachmentId);
|
|
740
|
+
if (options.imagesOnly === true && chatFile && !isImageChatFile(chatFile)) {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
downloadedFiles.push(await this.downloadChatFileAttachment(message.id, attachment, options));
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
const errMessage = error instanceof Error ? error.message : String(error);
|
|
748
|
+
errors.push(`message ${message.id}, file ${attachment.attachmentId}: ${errMessage}`);
|
|
749
|
+
downloadedFiles.push({
|
|
750
|
+
diskFileId: attachment.fileId ?? attachment.attachmentId,
|
|
751
|
+
attachmentId: attachment.attachmentId,
|
|
752
|
+
fileId: attachment.fileId,
|
|
753
|
+
fileName: attachment.name,
|
|
754
|
+
mimeType: guessMimeType(attachment.name),
|
|
755
|
+
isImage: chatFile ? isImageChatFile(chatFile) : isImageFile(attachment.name),
|
|
756
|
+
error: errMessage,
|
|
757
|
+
commentId: message.id
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
comments.push({
|
|
763
|
+
id: message.id,
|
|
764
|
+
authorId: message.authorId !== '0' ? message.authorId : undefined,
|
|
765
|
+
postDate: message.date,
|
|
766
|
+
postMessage: message.text,
|
|
767
|
+
postMessageHtml: null,
|
|
768
|
+
attachments: selectedAttachments,
|
|
769
|
+
downloadedFiles
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const orderDirection = Object.values(options.order ?? { POST_DATE: 'asc' })[0]?.toLowerCase();
|
|
773
|
+
if (orderDirection === 'desc') {
|
|
774
|
+
comments.reverse();
|
|
775
|
+
}
|
|
776
|
+
return { comments, errors };
|
|
777
|
+
}
|
|
778
|
+
async getTaskCommentsLegacy(taskId, options) {
|
|
779
|
+
const includeAttachments = options.includeAttachments !== false;
|
|
780
|
+
const requestedCommentIds = options.commentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
781
|
+
const requestedAttachmentIds = options.attachmentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
782
|
+
const rawItems = await this.fetchTaskCommentRawItems(taskId, options);
|
|
783
|
+
const filteredItems = requestedCommentIds.length > 0
|
|
784
|
+
? rawItems.filter((item) => requestedCommentIds.includes(String(item.ID ?? item.id ?? '')))
|
|
785
|
+
: rawItems;
|
|
786
|
+
const comments = [];
|
|
787
|
+
const errors = [];
|
|
788
|
+
for (const rawItem of filteredItems) {
|
|
789
|
+
const comment = normalizeTaskComment(rawItem);
|
|
790
|
+
if (!comment.id) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const attachments = extractCommentAttachments(rawItem);
|
|
794
|
+
const selectedAttachments = this.selectCommentAttachments(attachments, requestedAttachmentIds);
|
|
795
|
+
const downloadedFiles = [];
|
|
796
|
+
if (includeAttachments) {
|
|
797
|
+
for (const attachment of selectedAttachments) {
|
|
798
|
+
try {
|
|
799
|
+
if (options.imagesOnly === true && !isImageFile(attachment.name)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
downloadedFiles.push(await this.downloadCommentAttachment(comment.id, attachment, options));
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
806
|
+
errors.push(`comment ${comment.id}, attachment ${attachment.attachmentId}: ${message}`);
|
|
807
|
+
downloadedFiles.push({
|
|
808
|
+
diskFileId: attachment.fileId
|
|
809
|
+
? toWebdavFileToken(attachment.fileId)
|
|
810
|
+
: attachment.attachmentId,
|
|
811
|
+
attachmentId: attachment.attachmentId,
|
|
812
|
+
fileId: attachment.fileId,
|
|
813
|
+
fileName: attachment.name,
|
|
814
|
+
mimeType: guessMimeType(attachment.name),
|
|
815
|
+
isImage: isImageFile(attachment.name),
|
|
816
|
+
error: message,
|
|
817
|
+
commentId: comment.id
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
comments.push({
|
|
823
|
+
...comment,
|
|
824
|
+
attachments: selectedAttachments,
|
|
825
|
+
downloadedFiles
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
return { comments, errors };
|
|
829
|
+
}
|
|
830
|
+
async sendTaskCommentViaChat(taskId, chat, options) {
|
|
831
|
+
const text = (options.text ?? '').trim();
|
|
832
|
+
const filePaths = options.filePaths ?? [];
|
|
833
|
+
const uploadedDiskFileIds = [];
|
|
834
|
+
for (const filePath of filePaths) {
|
|
835
|
+
const { fileName, base64 } = await readLocalFileAsBase64(filePath);
|
|
836
|
+
const uploaded = await this.uploadFileToDisk({
|
|
837
|
+
folderId: options.diskFolderId,
|
|
838
|
+
fileName,
|
|
839
|
+
base64Content: base64
|
|
840
|
+
});
|
|
841
|
+
uploadedDiskFileIds.push(uploaded.diskObjectId);
|
|
842
|
+
}
|
|
843
|
+
if (uploadedDiskFileIds.length > 0) {
|
|
844
|
+
const commitResult = await this.makeRequest('im.disk.file.commit', {
|
|
845
|
+
CHAT_ID: Number(chat.chatId),
|
|
846
|
+
FILE_ID: uploadedDiskFileIds.map((id) => Number(id)),
|
|
847
|
+
MESSAGE: text || undefined
|
|
848
|
+
});
|
|
849
|
+
const messageId = commitResult?.MESSAGE_ID !== undefined
|
|
850
|
+
? String(commitResult.MESSAGE_ID)
|
|
851
|
+
: commitResult?.messageId !== undefined
|
|
852
|
+
? String(commitResult.messageId)
|
|
853
|
+
: undefined;
|
|
854
|
+
return { messageId, uploadedDiskFileIds };
|
|
855
|
+
}
|
|
856
|
+
if (!text) {
|
|
857
|
+
throw new Error('Comment text or filePaths is required');
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
await this.makeRequest('tasks.task.chat.message.send', {
|
|
861
|
+
fields: {
|
|
862
|
+
taskId: Number(taskId),
|
|
863
|
+
text
|
|
864
|
+
}
|
|
865
|
+
}, { useApiV3: true });
|
|
866
|
+
return { uploadedDiskFileIds };
|
|
867
|
+
}
|
|
868
|
+
catch (v3Error) {
|
|
869
|
+
console.error('tasks.task.chat.message.send failed, fallback to im.message.add:', v3Error);
|
|
870
|
+
}
|
|
871
|
+
const messageId = await this.makeRequest('im.message.add', {
|
|
872
|
+
DIALOG_ID: chat.dialogId,
|
|
873
|
+
MESSAGE: text,
|
|
874
|
+
SYSTEM: 'N'
|
|
875
|
+
});
|
|
876
|
+
return {
|
|
877
|
+
messageId: messageId !== undefined ? String(messageId) : undefined,
|
|
878
|
+
uploadedDiskFileIds
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
async sendTaskCommentLegacy(taskId, options) {
|
|
882
|
+
const text = (options.text ?? '').trim();
|
|
883
|
+
const filePaths = options.filePaths ?? [];
|
|
884
|
+
const uploadedDiskFileIds = [];
|
|
885
|
+
const forumDocs = [];
|
|
886
|
+
for (const filePath of filePaths) {
|
|
887
|
+
const { fileName, base64 } = await readLocalFileAsBase64(filePath);
|
|
888
|
+
const uploaded = await this.uploadFileToDisk({
|
|
889
|
+
folderId: options.diskFolderId,
|
|
890
|
+
fileName,
|
|
891
|
+
base64Content: base64
|
|
892
|
+
});
|
|
893
|
+
uploadedDiskFileIds.push(uploaded.diskObjectId);
|
|
894
|
+
forumDocs.push(toWebdavFileToken(uploaded.diskObjectId));
|
|
895
|
+
}
|
|
896
|
+
if (!text && forumDocs.length === 0) {
|
|
897
|
+
throw new Error('Comment text or filePaths is required');
|
|
898
|
+
}
|
|
899
|
+
const fields = {
|
|
900
|
+
POST_MESSAGE: text || (forumDocs.length > 0 ? ' ' : '')
|
|
901
|
+
};
|
|
902
|
+
if (options.authorId) {
|
|
903
|
+
fields.AUTHOR_ID = Number(options.authorId);
|
|
904
|
+
}
|
|
905
|
+
if (forumDocs.length > 0) {
|
|
906
|
+
fields.UF_FORUM_MESSAGE_DOC = forumDocs;
|
|
907
|
+
}
|
|
908
|
+
const commentId = await this.makeRequest('task.commentitem.add', {
|
|
909
|
+
TASKID: Number(taskId),
|
|
910
|
+
FIELDS: fields
|
|
911
|
+
});
|
|
912
|
+
return {
|
|
913
|
+
commentId: String(commentId),
|
|
914
|
+
uploadedDiskFileIds
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
async sendTaskComment(taskId, options) {
|
|
918
|
+
const preferApi = options.preferApi ?? 'auto';
|
|
919
|
+
const errors = [];
|
|
920
|
+
let chat = null;
|
|
921
|
+
if (preferApi !== 'legacy') {
|
|
922
|
+
chat = await this.resolveTaskChat(taskId);
|
|
923
|
+
}
|
|
924
|
+
if (chat && preferApi !== 'legacy') {
|
|
925
|
+
try {
|
|
926
|
+
const sent = await this.sendTaskCommentViaChat(taskId, chat, options);
|
|
927
|
+
return {
|
|
928
|
+
success: true,
|
|
929
|
+
apiMode: 'chat',
|
|
930
|
+
taskId,
|
|
931
|
+
chatId: chat.chatId,
|
|
932
|
+
messageId: sent.messageId,
|
|
933
|
+
uploadedDiskFileIds: sent.uploadedDiskFileIds,
|
|
934
|
+
errors
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
939
|
+
errors.push(`chat API: ${message}`);
|
|
940
|
+
if (preferApi === 'chat') {
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
apiMode: 'chat',
|
|
944
|
+
taskId,
|
|
945
|
+
chatId: chat.chatId,
|
|
946
|
+
uploadedDiskFileIds: [],
|
|
947
|
+
errors
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const sent = await this.sendTaskCommentLegacy(taskId, options);
|
|
954
|
+
return {
|
|
955
|
+
success: true,
|
|
956
|
+
apiMode: 'legacy',
|
|
957
|
+
taskId,
|
|
958
|
+
commentId: sent.commentId,
|
|
959
|
+
uploadedDiskFileIds: sent.uploadedDiskFileIds,
|
|
960
|
+
errors
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
965
|
+
errors.push(`legacy API: ${message}`);
|
|
966
|
+
return {
|
|
967
|
+
success: false,
|
|
968
|
+
apiMode: 'legacy',
|
|
969
|
+
taskId,
|
|
970
|
+
chatId: chat?.chatId,
|
|
971
|
+
uploadedDiskFileIds: [],
|
|
972
|
+
errors
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
matchesRequestedAttachmentId(attachment, requestedId) {
|
|
977
|
+
const normalized = requestedId.replace(/^n/i, '').trim();
|
|
978
|
+
if (!normalized) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
return (attachment.attachmentId === normalized ||
|
|
982
|
+
attachment.fileId === normalized ||
|
|
983
|
+
(requestedId.startsWith('n') && attachment.fileId === normalized));
|
|
984
|
+
}
|
|
985
|
+
selectCommentAttachments(attachments, requestedIds) {
|
|
986
|
+
if (requestedIds.length === 0) {
|
|
987
|
+
return attachments;
|
|
988
|
+
}
|
|
989
|
+
const selected = [];
|
|
990
|
+
for (const requestedId of requestedIds) {
|
|
991
|
+
const match = attachments.find((file) => this.matchesRequestedAttachmentId(file, requestedId));
|
|
992
|
+
if (match && !selected.some((item) => item.attachmentId === match.attachmentId)) {
|
|
993
|
+
selected.push(match);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return selected;
|
|
997
|
+
}
|
|
998
|
+
async downloadCommentAttachment(commentId, attached, options) {
|
|
999
|
+
const taskAttached = {
|
|
1000
|
+
attachmentId: attached.attachmentId,
|
|
1001
|
+
fileId: attached.fileId,
|
|
1002
|
+
name: attached.name,
|
|
1003
|
+
size: attached.size,
|
|
1004
|
+
downloadUrl: attached.downloadUrl,
|
|
1005
|
+
viewUrl: attached.viewUrl
|
|
1006
|
+
};
|
|
1007
|
+
const downloaded = await this.downloadTaskAttachedFile(taskAttached, options);
|
|
1008
|
+
return {
|
|
1009
|
+
...downloaded,
|
|
1010
|
+
commentId
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
async getTaskComments(taskId, options = {}) {
|
|
1014
|
+
const preferApi = options.preferApi ?? 'auto';
|
|
1015
|
+
const errors = [];
|
|
1016
|
+
let chat = null;
|
|
1017
|
+
if (preferApi !== 'legacy') {
|
|
1018
|
+
chat = await this.resolveTaskChat(taskId);
|
|
1019
|
+
}
|
|
1020
|
+
if (chat && preferApi !== 'legacy') {
|
|
1021
|
+
try {
|
|
1022
|
+
const chatResult = await this.getTaskCommentsFromChat(taskId, chat, options);
|
|
1023
|
+
return {
|
|
1024
|
+
taskId,
|
|
1025
|
+
apiMode: 'chat',
|
|
1026
|
+
chatId: chat.chatId,
|
|
1027
|
+
dialogId: chat.dialogId,
|
|
1028
|
+
comments: chatResult.comments,
|
|
1029
|
+
errors: [...errors, ...chatResult.errors]
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1034
|
+
errors.push(`chat API: ${message}`);
|
|
1035
|
+
if (preferApi === 'chat') {
|
|
1036
|
+
return {
|
|
1037
|
+
taskId,
|
|
1038
|
+
apiMode: 'chat',
|
|
1039
|
+
chatId: chat.chatId,
|
|
1040
|
+
dialogId: chat.dialogId,
|
|
1041
|
+
comments: [],
|
|
1042
|
+
errors
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const legacyResult = await this.getTaskCommentsLegacy(taskId, options);
|
|
1048
|
+
return {
|
|
1049
|
+
taskId,
|
|
1050
|
+
apiMode: 'legacy',
|
|
1051
|
+
chatId: chat?.chatId,
|
|
1052
|
+
dialogId: chat?.dialogId,
|
|
1053
|
+
comments: legacyResult.comments,
|
|
1054
|
+
errors: [...errors, ...legacyResult.errors]
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
390
1057
|
// Project Methods (Bitrix24 workgroups)
|
|
391
1058
|
async createProject(project) {
|
|
392
1059
|
const result = await this.makeRequest('sonet_group.create', { fields: project });
|
|
@@ -549,6 +1216,7 @@ export class Bitrix24Client {
|
|
|
549
1216
|
tasks_access: false,
|
|
550
1217
|
projects_access: false,
|
|
551
1218
|
disk_access: false,
|
|
1219
|
+
chat_access: false,
|
|
552
1220
|
error_details: []
|
|
553
1221
|
};
|
|
554
1222
|
try {
|
|
@@ -571,6 +1239,9 @@ export class Bitrix24Client {
|
|
|
571
1239
|
if (scopes.includes('disk')) {
|
|
572
1240
|
results.disk_access = true;
|
|
573
1241
|
}
|
|
1242
|
+
if (scopes.includes('im')) {
|
|
1243
|
+
results.chat_access = true;
|
|
1244
|
+
}
|
|
574
1245
|
try {
|
|
575
1246
|
await this.getLatestTasks(1);
|
|
576
1247
|
results.tasks_access = true;
|
|
@@ -596,6 +1267,25 @@ export class Bitrix24Client {
|
|
|
596
1267
|
results.error_details.push(`disk access failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
597
1268
|
}
|
|
598
1269
|
}
|
|
1270
|
+
try {
|
|
1271
|
+
const latestTasks = await this.getLatestTasks(1);
|
|
1272
|
+
const sampleTaskId = latestTasks[0]?.ID;
|
|
1273
|
+
if (sampleTaskId) {
|
|
1274
|
+
const chat = await this.resolveTaskChat(String(sampleTaskId));
|
|
1275
|
+
if (chat) {
|
|
1276
|
+
await this.makeRequest('im.dialog.messages.get', {
|
|
1277
|
+
DIALOG_ID: chat.dialogId,
|
|
1278
|
+
LIMIT: 1
|
|
1279
|
+
});
|
|
1280
|
+
results.chat_access = true;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
catch (error) {
|
|
1285
|
+
if (!results.chat_access) {
|
|
1286
|
+
results.error_details.push(`chat access failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
599
1289
|
return results;
|
|
600
1290
|
}
|
|
601
1291
|
}
|