bitrix24-tasks-mcp-server 1.2.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 +29 -1
- package/build/bitrix24/client.d.ts +70 -0
- package/build/bitrix24/client.d.ts.map +1 -1
- package/build/bitrix24/client.js +510 -6
- 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/index.js +8 -3
- package/build/index.js.map +1 -1
- package/build/tools/index.d.ts +2 -0
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +174 -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
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({
|
|
@@ -28,6 +30,35 @@ export class Bitrix24Client {
|
|
|
28
30
|
const match = this.baseUrl.match(/^(https?:\/\/[^/]+)/i);
|
|
29
31
|
return match?.[1] ?? '';
|
|
30
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
|
+
}
|
|
31
62
|
async enforceRateLimit() {
|
|
32
63
|
const now = Date.now();
|
|
33
64
|
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
@@ -36,9 +67,12 @@ export class Bitrix24Client {
|
|
|
36
67
|
}
|
|
37
68
|
this.lastRequestTime = Date.now();
|
|
38
69
|
}
|
|
39
|
-
async
|
|
70
|
+
async callRest(method, params = {}, options = {}) {
|
|
71
|
+
return this.makeRequest(method, params, options);
|
|
72
|
+
}
|
|
73
|
+
async makeRequest(method, params = {}, options = {}) {
|
|
40
74
|
await this.enforceRateLimit();
|
|
41
|
-
const url =
|
|
75
|
+
const url = this.buildMethodUrl(method, options.useApiV3 === true);
|
|
42
76
|
try {
|
|
43
77
|
let response;
|
|
44
78
|
if (Object.keys(params).length === 0) {
|
|
@@ -68,11 +102,15 @@ export class Bitrix24Client {
|
|
|
68
102
|
console.error(`HTTP Error ${response.status}:`, errorText);
|
|
69
103
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
70
104
|
}
|
|
71
|
-
const data = await response.json();
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
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}`);
|
|
75
109
|
}
|
|
110
|
+
if (!('result' in data)) {
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
const parsed = BitrixResponseSchema.parse(data);
|
|
76
114
|
return parsed.result;
|
|
77
115
|
}
|
|
78
116
|
catch (error) {
|
|
@@ -573,6 +611,449 @@ export class Bitrix24Client {
|
|
|
573
611
|
}
|
|
574
612
|
return { taskId, diskFileIds: uniqueIds, files, errors };
|
|
575
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
|
+
}
|
|
576
1057
|
// Project Methods (Bitrix24 workgroups)
|
|
577
1058
|
async createProject(project) {
|
|
578
1059
|
const result = await this.makeRequest('sonet_group.create', { fields: project });
|
|
@@ -735,6 +1216,7 @@ export class Bitrix24Client {
|
|
|
735
1216
|
tasks_access: false,
|
|
736
1217
|
projects_access: false,
|
|
737
1218
|
disk_access: false,
|
|
1219
|
+
chat_access: false,
|
|
738
1220
|
error_details: []
|
|
739
1221
|
};
|
|
740
1222
|
try {
|
|
@@ -757,6 +1239,9 @@ export class Bitrix24Client {
|
|
|
757
1239
|
if (scopes.includes('disk')) {
|
|
758
1240
|
results.disk_access = true;
|
|
759
1241
|
}
|
|
1242
|
+
if (scopes.includes('im')) {
|
|
1243
|
+
results.chat_access = true;
|
|
1244
|
+
}
|
|
760
1245
|
try {
|
|
761
1246
|
await this.getLatestTasks(1);
|
|
762
1247
|
results.tasks_access = true;
|
|
@@ -782,6 +1267,25 @@ export class Bitrix24Client {
|
|
|
782
1267
|
results.error_details.push(`disk access failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
783
1268
|
}
|
|
784
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
|
+
}
|
|
785
1289
|
return results;
|
|
786
1290
|
}
|
|
787
1291
|
}
|