bitrix24-tasks-mcp-server 1.2.0 → 1.5.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 +35 -4
- package/build/bitrix24/client.d.ts +101 -0
- package/build/bitrix24/client.d.ts.map +1 -1
- package/build/bitrix24/client.js +590 -10
- 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 +3 -0
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +280 -4
- 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}`);
|
|
109
|
+
}
|
|
110
|
+
if (!('result' in data)) {
|
|
111
|
+
return data;
|
|
75
112
|
}
|
|
113
|
+
const parsed = BitrixResponseSchema.parse(data);
|
|
76
114
|
return parsed.result;
|
|
77
115
|
}
|
|
78
116
|
catch (error) {
|
|
@@ -112,6 +150,67 @@ export class Bitrix24Client {
|
|
|
112
150
|
const result = await this.makeRequest('tasks.task.get', { taskId: id });
|
|
113
151
|
return result.task;
|
|
114
152
|
}
|
|
153
|
+
async getTaskDetails(taskId, options = {}) {
|
|
154
|
+
const task = await this.getTask(taskId);
|
|
155
|
+
const errors = [];
|
|
156
|
+
const includeComments = options.includeComments !== false;
|
|
157
|
+
const includeTaskFiles = options.includeTaskFiles !== false;
|
|
158
|
+
const includeContent = options.includeContent !== false;
|
|
159
|
+
const includeAttachments = options.includeCommentAttachments !== false;
|
|
160
|
+
let comments;
|
|
161
|
+
if (includeComments) {
|
|
162
|
+
try {
|
|
163
|
+
const commentsResult = await this.getTaskComments(taskId, {
|
|
164
|
+
preferApi: options.preferApi,
|
|
165
|
+
limit: options.commentsLimit,
|
|
166
|
+
includeSystemMessages: options.includeSystemMessages,
|
|
167
|
+
commentIds: options.commentIds,
|
|
168
|
+
includeAttachments,
|
|
169
|
+
attachmentIds: options.attachmentIds,
|
|
170
|
+
includeContent,
|
|
171
|
+
imagesOnly: options.imagesOnly,
|
|
172
|
+
saveToDirectory: options.saveToDirectory
|
|
173
|
+
});
|
|
174
|
+
comments = {
|
|
175
|
+
apiMode: commentsResult.apiMode,
|
|
176
|
+
chatId: commentsResult.chatId,
|
|
177
|
+
dialogId: commentsResult.dialogId,
|
|
178
|
+
comments: commentsResult.comments,
|
|
179
|
+
errors: commentsResult.errors
|
|
180
|
+
};
|
|
181
|
+
if (commentsResult.errors.length > 0) {
|
|
182
|
+
errors.push(...commentsResult.errors.map((e) => `comments: ${e}`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
errors.push(`comments: ${message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let taskFiles;
|
|
191
|
+
if (includeTaskFiles) {
|
|
192
|
+
try {
|
|
193
|
+
const filesResult = await this.getTaskFiles(taskId, {
|
|
194
|
+
includeContent,
|
|
195
|
+
imagesOnly: options.imagesOnly,
|
|
196
|
+
saveToDirectory: options.saveToDirectory
|
|
197
|
+
});
|
|
198
|
+
taskFiles = {
|
|
199
|
+
diskFileIds: filesResult.diskFileIds,
|
|
200
|
+
files: filesResult.files,
|
|
201
|
+
errors: filesResult.errors
|
|
202
|
+
};
|
|
203
|
+
if (filesResult.errors.length > 0) {
|
|
204
|
+
errors.push(...filesResult.errors.map((e) => `taskFiles: ${e}`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
errors.push(`taskFiles: ${message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { task, comments, taskFiles, errors };
|
|
213
|
+
}
|
|
115
214
|
async updateTask(id, task) {
|
|
116
215
|
const result = await this.makeRequest('tasks.task.update', { taskId: id, fields: task });
|
|
117
216
|
return result === true;
|
|
@@ -484,11 +583,26 @@ export class Bitrix24Client {
|
|
|
484
583
|
collectTaskDiskFileIds(task) {
|
|
485
584
|
return collectDiskFileIdsFromTask(task);
|
|
486
585
|
}
|
|
586
|
+
async resolveAttachmentDownloadUrl(attached) {
|
|
587
|
+
if (attached.fileId) {
|
|
588
|
+
try {
|
|
589
|
+
const diskFile = await this.getDiskFile(parseWebdavFileToken(attached.fileId));
|
|
590
|
+
if (diskFile.downloadUrl) {
|
|
591
|
+
return diskFile.downloadUrl;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
console.error(`disk.file.get failed for file ${attached.fileId} (${attached.name}):`, error);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return attached.downloadUrl;
|
|
599
|
+
}
|
|
487
600
|
async downloadTaskAttachedFile(attached, options) {
|
|
488
601
|
const includeContent = options.includeContent !== false;
|
|
489
602
|
const imagesOnly = options.imagesOnly === true;
|
|
490
603
|
const isImage = isImageFile(attached.name);
|
|
491
604
|
const diskFileId = attached.fileId ? toWebdavFileToken(attached.fileId) : attached.attachmentId;
|
|
605
|
+
const downloadUrl = await this.resolveAttachmentDownloadUrl(attached);
|
|
492
606
|
const entry = {
|
|
493
607
|
diskFileId,
|
|
494
608
|
attachmentId: attached.attachmentId,
|
|
@@ -497,12 +611,12 @@ export class Bitrix24Client {
|
|
|
497
611
|
mimeType: guessMimeType(attached.name),
|
|
498
612
|
size: attached.size,
|
|
499
613
|
isImage,
|
|
500
|
-
downloadUrl
|
|
614
|
+
downloadUrl,
|
|
501
615
|
viewUrl: attached.viewUrl
|
|
502
616
|
};
|
|
503
|
-
const shouldDownload = includeContent &&
|
|
504
|
-
if (shouldDownload &&
|
|
505
|
-
const buffer = await this.downloadDiskFileContent(
|
|
617
|
+
const shouldDownload = includeContent && downloadUrl && (!imagesOnly || isImage);
|
|
618
|
+
if (shouldDownload && downloadUrl) {
|
|
619
|
+
const buffer = await this.downloadDiskFileContent(downloadUrl);
|
|
506
620
|
if (options.saveToDirectory) {
|
|
507
621
|
entry.savedPath = await saveBufferToDirectory(options.saveToDirectory, attached.name, buffer);
|
|
508
622
|
}
|
|
@@ -573,6 +687,449 @@ export class Bitrix24Client {
|
|
|
573
687
|
}
|
|
574
688
|
return { taskId, diskFileIds: uniqueIds, files, errors };
|
|
575
689
|
}
|
|
690
|
+
async resolveTaskChat(taskId) {
|
|
691
|
+
try {
|
|
692
|
+
const legacy = await this.makeRequest('tasks.task.get', {
|
|
693
|
+
taskId,
|
|
694
|
+
select: ['ID', 'CHAT_ID']
|
|
695
|
+
});
|
|
696
|
+
const chatId = legacy?.task?.CHAT_ID ?? legacy?.task?.chatId;
|
|
697
|
+
if (chatId !== undefined && chatId !== null && chatId !== '') {
|
|
698
|
+
return { chatId: String(chatId), dialogId: buildDialogId(chatId) };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
console.error(`tasks.task.get CHAT_ID failed for task ${taskId}:`, error);
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const modern = await this.makeRequest('tasks.task.get', {
|
|
706
|
+
id: Number(taskId),
|
|
707
|
+
select: ['id', 'chat.id']
|
|
708
|
+
}, { useApiV3: true });
|
|
709
|
+
const chatId = modern?.item?.chat?.id ?? modern?.chat?.id;
|
|
710
|
+
if (chatId !== undefined && chatId !== null && chatId !== '') {
|
|
711
|
+
return { chatId: String(chatId), dialogId: buildDialogId(chatId) };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
console.error(`tasks.task.get (v3) chat.id failed for task ${taskId}:`, error);
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
async listTaskComments(taskId, options = {}) {
|
|
720
|
+
const payload = {
|
|
721
|
+
TASKID: Number(taskId)
|
|
722
|
+
};
|
|
723
|
+
if (options.order && Object.keys(options.order).length > 0) {
|
|
724
|
+
payload.ORDER = options.order;
|
|
725
|
+
}
|
|
726
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
727
|
+
payload.FILTER = options.filter;
|
|
728
|
+
}
|
|
729
|
+
const result = await this.makeRequest('task.commentitem.getlist', payload);
|
|
730
|
+
if (!Array.isArray(result)) {
|
|
731
|
+
return [];
|
|
732
|
+
}
|
|
733
|
+
return result
|
|
734
|
+
.map((item) => normalizeTaskComment(item))
|
|
735
|
+
.filter((comment) => comment.id);
|
|
736
|
+
}
|
|
737
|
+
async getTaskComment(taskId, commentId) {
|
|
738
|
+
const result = await this.makeRequest('task.commentitem.get', {
|
|
739
|
+
TASKID: Number(taskId),
|
|
740
|
+
ITEMID: Number(commentId)
|
|
741
|
+
});
|
|
742
|
+
if (!result || typeof result !== 'object') {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
const comment = normalizeTaskComment(result);
|
|
746
|
+
return comment.id ? comment : null;
|
|
747
|
+
}
|
|
748
|
+
async fetchTaskCommentRawItems(taskId, options) {
|
|
749
|
+
const payload = {
|
|
750
|
+
TASKID: Number(taskId)
|
|
751
|
+
};
|
|
752
|
+
if (options.order && Object.keys(options.order).length > 0) {
|
|
753
|
+
payload.ORDER = options.order;
|
|
754
|
+
}
|
|
755
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
756
|
+
payload.FILTER = options.filter;
|
|
757
|
+
}
|
|
758
|
+
const result = await this.makeRequest('task.commentitem.getlist', payload);
|
|
759
|
+
if (!Array.isArray(result)) {
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
async downloadChatFileAttachment(commentId, file, options) {
|
|
765
|
+
const attached = {
|
|
766
|
+
attachmentId: file.attachmentId,
|
|
767
|
+
fileId: file.fileId,
|
|
768
|
+
name: file.name,
|
|
769
|
+
size: file.size,
|
|
770
|
+
downloadUrl: file.downloadUrl,
|
|
771
|
+
viewUrl: file.viewUrl
|
|
772
|
+
};
|
|
773
|
+
const downloaded = await this.downloadTaskAttachedFile(attached, options);
|
|
774
|
+
return { ...downloaded, commentId };
|
|
775
|
+
}
|
|
776
|
+
async getTaskCommentsFromChat(taskId, chat, options) {
|
|
777
|
+
const payload = {
|
|
778
|
+
DIALOG_ID: chat.dialogId,
|
|
779
|
+
LIMIT: Math.min(Math.max(options.limit ?? 50, 1), 50)
|
|
780
|
+
};
|
|
781
|
+
if (options.lastId !== undefined) {
|
|
782
|
+
payload.LAST_ID = options.lastId;
|
|
783
|
+
}
|
|
784
|
+
if (options.firstId !== undefined) {
|
|
785
|
+
payload.FIRST_ID = options.firstId;
|
|
786
|
+
}
|
|
787
|
+
const result = await this.makeRequest('im.dialog.messages.get', payload);
|
|
788
|
+
const messagesRaw = Array.isArray(result?.messages) ? result.messages : [];
|
|
789
|
+
const filesRaw = Array.isArray(result?.files) ? result.files : [];
|
|
790
|
+
const files = filesRaw
|
|
791
|
+
.map((item) => normalizeChatFile(item))
|
|
792
|
+
.filter((item) => item !== null);
|
|
793
|
+
const filesById = indexChatFiles(files);
|
|
794
|
+
const includeAttachments = options.includeAttachments !== false;
|
|
795
|
+
const requestedCommentIds = options.commentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
796
|
+
const requestedAttachmentIds = options.attachmentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
797
|
+
const comments = [];
|
|
798
|
+
const errors = [];
|
|
799
|
+
for (const rawMessage of messagesRaw) {
|
|
800
|
+
const message = normalizeChatMessage(rawMessage);
|
|
801
|
+
if (!message) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (!options.includeSystemMessages && message.isSystem) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (requestedCommentIds.length > 0 && !requestedCommentIds.includes(message.id)) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const attachments = resolveMessageAttachments(message, filesById);
|
|
811
|
+
const selectedAttachments = this.selectCommentAttachments(attachments, requestedAttachmentIds);
|
|
812
|
+
const downloadedFiles = [];
|
|
813
|
+
if (includeAttachments) {
|
|
814
|
+
for (const attachment of selectedAttachments) {
|
|
815
|
+
const chatFile = filesById.get(attachment.fileId ?? attachment.attachmentId);
|
|
816
|
+
if (options.imagesOnly === true && chatFile && !isImageChatFile(chatFile)) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
downloadedFiles.push(await this.downloadChatFileAttachment(message.id, attachment, options));
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
const errMessage = error instanceof Error ? error.message : String(error);
|
|
824
|
+
errors.push(`message ${message.id}, file ${attachment.attachmentId}: ${errMessage}`);
|
|
825
|
+
downloadedFiles.push({
|
|
826
|
+
diskFileId: attachment.fileId ?? attachment.attachmentId,
|
|
827
|
+
attachmentId: attachment.attachmentId,
|
|
828
|
+
fileId: attachment.fileId,
|
|
829
|
+
fileName: attachment.name,
|
|
830
|
+
mimeType: guessMimeType(attachment.name),
|
|
831
|
+
isImage: chatFile ? isImageChatFile(chatFile) : isImageFile(attachment.name),
|
|
832
|
+
error: errMessage,
|
|
833
|
+
commentId: message.id
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
comments.push({
|
|
839
|
+
id: message.id,
|
|
840
|
+
authorId: message.authorId !== '0' ? message.authorId : undefined,
|
|
841
|
+
postDate: message.date,
|
|
842
|
+
postMessage: message.text,
|
|
843
|
+
postMessageHtml: null,
|
|
844
|
+
attachments: selectedAttachments,
|
|
845
|
+
downloadedFiles
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
const orderDirection = Object.values(options.order ?? { POST_DATE: 'asc' })[0]?.toLowerCase();
|
|
849
|
+
if (orderDirection === 'desc') {
|
|
850
|
+
comments.reverse();
|
|
851
|
+
}
|
|
852
|
+
return { comments, errors };
|
|
853
|
+
}
|
|
854
|
+
async getTaskCommentsLegacy(taskId, options) {
|
|
855
|
+
const includeAttachments = options.includeAttachments !== false;
|
|
856
|
+
const requestedCommentIds = options.commentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
857
|
+
const requestedAttachmentIds = options.attachmentIds?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
858
|
+
const rawItems = await this.fetchTaskCommentRawItems(taskId, options);
|
|
859
|
+
const filteredItems = requestedCommentIds.length > 0
|
|
860
|
+
? rawItems.filter((item) => requestedCommentIds.includes(String(item.ID ?? item.id ?? '')))
|
|
861
|
+
: rawItems;
|
|
862
|
+
const comments = [];
|
|
863
|
+
const errors = [];
|
|
864
|
+
for (const rawItem of filteredItems) {
|
|
865
|
+
const comment = normalizeTaskComment(rawItem);
|
|
866
|
+
if (!comment.id) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const attachments = extractCommentAttachments(rawItem);
|
|
870
|
+
const selectedAttachments = this.selectCommentAttachments(attachments, requestedAttachmentIds);
|
|
871
|
+
const downloadedFiles = [];
|
|
872
|
+
if (includeAttachments) {
|
|
873
|
+
for (const attachment of selectedAttachments) {
|
|
874
|
+
try {
|
|
875
|
+
if (options.imagesOnly === true && !isImageFile(attachment.name)) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
downloadedFiles.push(await this.downloadCommentAttachment(comment.id, attachment, options));
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
882
|
+
errors.push(`comment ${comment.id}, attachment ${attachment.attachmentId}: ${message}`);
|
|
883
|
+
downloadedFiles.push({
|
|
884
|
+
diskFileId: attachment.fileId
|
|
885
|
+
? toWebdavFileToken(attachment.fileId)
|
|
886
|
+
: attachment.attachmentId,
|
|
887
|
+
attachmentId: attachment.attachmentId,
|
|
888
|
+
fileId: attachment.fileId,
|
|
889
|
+
fileName: attachment.name,
|
|
890
|
+
mimeType: guessMimeType(attachment.name),
|
|
891
|
+
isImage: isImageFile(attachment.name),
|
|
892
|
+
error: message,
|
|
893
|
+
commentId: comment.id
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
comments.push({
|
|
899
|
+
...comment,
|
|
900
|
+
attachments: selectedAttachments,
|
|
901
|
+
downloadedFiles
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
return { comments, errors };
|
|
905
|
+
}
|
|
906
|
+
async sendTaskCommentViaChat(taskId, chat, options) {
|
|
907
|
+
const text = (options.text ?? '').trim();
|
|
908
|
+
const filePaths = options.filePaths ?? [];
|
|
909
|
+
const uploadedDiskFileIds = [];
|
|
910
|
+
for (const filePath of filePaths) {
|
|
911
|
+
const { fileName, base64 } = await readLocalFileAsBase64(filePath);
|
|
912
|
+
const uploaded = await this.uploadFileToDisk({
|
|
913
|
+
folderId: options.diskFolderId,
|
|
914
|
+
fileName,
|
|
915
|
+
base64Content: base64
|
|
916
|
+
});
|
|
917
|
+
uploadedDiskFileIds.push(uploaded.diskObjectId);
|
|
918
|
+
}
|
|
919
|
+
if (uploadedDiskFileIds.length > 0) {
|
|
920
|
+
const commitResult = await this.makeRequest('im.disk.file.commit', {
|
|
921
|
+
CHAT_ID: Number(chat.chatId),
|
|
922
|
+
FILE_ID: uploadedDiskFileIds.map((id) => Number(id)),
|
|
923
|
+
MESSAGE: text || undefined
|
|
924
|
+
});
|
|
925
|
+
const messageId = commitResult?.MESSAGE_ID !== undefined
|
|
926
|
+
? String(commitResult.MESSAGE_ID)
|
|
927
|
+
: commitResult?.messageId !== undefined
|
|
928
|
+
? String(commitResult.messageId)
|
|
929
|
+
: undefined;
|
|
930
|
+
return { messageId, uploadedDiskFileIds };
|
|
931
|
+
}
|
|
932
|
+
if (!text) {
|
|
933
|
+
throw new Error('Comment text or filePaths is required');
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
await this.makeRequest('tasks.task.chat.message.send', {
|
|
937
|
+
fields: {
|
|
938
|
+
taskId: Number(taskId),
|
|
939
|
+
text
|
|
940
|
+
}
|
|
941
|
+
}, { useApiV3: true });
|
|
942
|
+
return { uploadedDiskFileIds };
|
|
943
|
+
}
|
|
944
|
+
catch (v3Error) {
|
|
945
|
+
console.error('tasks.task.chat.message.send failed, fallback to im.message.add:', v3Error);
|
|
946
|
+
}
|
|
947
|
+
const messageId = await this.makeRequest('im.message.add', {
|
|
948
|
+
DIALOG_ID: chat.dialogId,
|
|
949
|
+
MESSAGE: text,
|
|
950
|
+
SYSTEM: 'N'
|
|
951
|
+
});
|
|
952
|
+
return {
|
|
953
|
+
messageId: messageId !== undefined ? String(messageId) : undefined,
|
|
954
|
+
uploadedDiskFileIds
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
async sendTaskCommentLegacy(taskId, options) {
|
|
958
|
+
const text = (options.text ?? '').trim();
|
|
959
|
+
const filePaths = options.filePaths ?? [];
|
|
960
|
+
const uploadedDiskFileIds = [];
|
|
961
|
+
const forumDocs = [];
|
|
962
|
+
for (const filePath of filePaths) {
|
|
963
|
+
const { fileName, base64 } = await readLocalFileAsBase64(filePath);
|
|
964
|
+
const uploaded = await this.uploadFileToDisk({
|
|
965
|
+
folderId: options.diskFolderId,
|
|
966
|
+
fileName,
|
|
967
|
+
base64Content: base64
|
|
968
|
+
});
|
|
969
|
+
uploadedDiskFileIds.push(uploaded.diskObjectId);
|
|
970
|
+
forumDocs.push(toWebdavFileToken(uploaded.diskObjectId));
|
|
971
|
+
}
|
|
972
|
+
if (!text && forumDocs.length === 0) {
|
|
973
|
+
throw new Error('Comment text or filePaths is required');
|
|
974
|
+
}
|
|
975
|
+
const fields = {
|
|
976
|
+
POST_MESSAGE: text || (forumDocs.length > 0 ? ' ' : '')
|
|
977
|
+
};
|
|
978
|
+
if (options.authorId) {
|
|
979
|
+
fields.AUTHOR_ID = Number(options.authorId);
|
|
980
|
+
}
|
|
981
|
+
if (forumDocs.length > 0) {
|
|
982
|
+
fields.UF_FORUM_MESSAGE_DOC = forumDocs;
|
|
983
|
+
}
|
|
984
|
+
const commentId = await this.makeRequest('task.commentitem.add', {
|
|
985
|
+
TASKID: Number(taskId),
|
|
986
|
+
FIELDS: fields
|
|
987
|
+
});
|
|
988
|
+
return {
|
|
989
|
+
commentId: String(commentId),
|
|
990
|
+
uploadedDiskFileIds
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
async sendTaskComment(taskId, options) {
|
|
994
|
+
const preferApi = options.preferApi ?? 'auto';
|
|
995
|
+
const errors = [];
|
|
996
|
+
let chat = null;
|
|
997
|
+
if (preferApi !== 'legacy') {
|
|
998
|
+
chat = await this.resolveTaskChat(taskId);
|
|
999
|
+
}
|
|
1000
|
+
if (chat && preferApi !== 'legacy') {
|
|
1001
|
+
try {
|
|
1002
|
+
const sent = await this.sendTaskCommentViaChat(taskId, chat, options);
|
|
1003
|
+
return {
|
|
1004
|
+
success: true,
|
|
1005
|
+
apiMode: 'chat',
|
|
1006
|
+
taskId,
|
|
1007
|
+
chatId: chat.chatId,
|
|
1008
|
+
messageId: sent.messageId,
|
|
1009
|
+
uploadedDiskFileIds: sent.uploadedDiskFileIds,
|
|
1010
|
+
errors
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1015
|
+
errors.push(`chat API: ${message}`);
|
|
1016
|
+
if (preferApi === 'chat') {
|
|
1017
|
+
return {
|
|
1018
|
+
success: false,
|
|
1019
|
+
apiMode: 'chat',
|
|
1020
|
+
taskId,
|
|
1021
|
+
chatId: chat.chatId,
|
|
1022
|
+
uploadedDiskFileIds: [],
|
|
1023
|
+
errors
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
const sent = await this.sendTaskCommentLegacy(taskId, options);
|
|
1030
|
+
return {
|
|
1031
|
+
success: true,
|
|
1032
|
+
apiMode: 'legacy',
|
|
1033
|
+
taskId,
|
|
1034
|
+
commentId: sent.commentId,
|
|
1035
|
+
uploadedDiskFileIds: sent.uploadedDiskFileIds,
|
|
1036
|
+
errors
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
catch (error) {
|
|
1040
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1041
|
+
errors.push(`legacy API: ${message}`);
|
|
1042
|
+
return {
|
|
1043
|
+
success: false,
|
|
1044
|
+
apiMode: 'legacy',
|
|
1045
|
+
taskId,
|
|
1046
|
+
chatId: chat?.chatId,
|
|
1047
|
+
uploadedDiskFileIds: [],
|
|
1048
|
+
errors
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
matchesRequestedAttachmentId(attachment, requestedId) {
|
|
1053
|
+
const normalized = requestedId.replace(/^n/i, '').trim();
|
|
1054
|
+
if (!normalized) {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
return (attachment.attachmentId === normalized ||
|
|
1058
|
+
attachment.fileId === normalized ||
|
|
1059
|
+
(requestedId.startsWith('n') && attachment.fileId === normalized));
|
|
1060
|
+
}
|
|
1061
|
+
selectCommentAttachments(attachments, requestedIds) {
|
|
1062
|
+
if (requestedIds.length === 0) {
|
|
1063
|
+
return attachments;
|
|
1064
|
+
}
|
|
1065
|
+
const selected = [];
|
|
1066
|
+
for (const requestedId of requestedIds) {
|
|
1067
|
+
const match = attachments.find((file) => this.matchesRequestedAttachmentId(file, requestedId));
|
|
1068
|
+
if (match && !selected.some((item) => item.attachmentId === match.attachmentId)) {
|
|
1069
|
+
selected.push(match);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return selected;
|
|
1073
|
+
}
|
|
1074
|
+
async downloadCommentAttachment(commentId, attached, options) {
|
|
1075
|
+
const taskAttached = {
|
|
1076
|
+
attachmentId: attached.attachmentId,
|
|
1077
|
+
fileId: attached.fileId,
|
|
1078
|
+
name: attached.name,
|
|
1079
|
+
size: attached.size,
|
|
1080
|
+
downloadUrl: attached.downloadUrl,
|
|
1081
|
+
viewUrl: attached.viewUrl
|
|
1082
|
+
};
|
|
1083
|
+
const downloaded = await this.downloadTaskAttachedFile(taskAttached, options);
|
|
1084
|
+
return {
|
|
1085
|
+
...downloaded,
|
|
1086
|
+
commentId
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
async getTaskComments(taskId, options = {}) {
|
|
1090
|
+
const preferApi = options.preferApi ?? 'auto';
|
|
1091
|
+
const errors = [];
|
|
1092
|
+
let chat = null;
|
|
1093
|
+
if (preferApi !== 'legacy') {
|
|
1094
|
+
chat = await this.resolveTaskChat(taskId);
|
|
1095
|
+
}
|
|
1096
|
+
if (chat && preferApi !== 'legacy') {
|
|
1097
|
+
try {
|
|
1098
|
+
const chatResult = await this.getTaskCommentsFromChat(taskId, chat, options);
|
|
1099
|
+
return {
|
|
1100
|
+
taskId,
|
|
1101
|
+
apiMode: 'chat',
|
|
1102
|
+
chatId: chat.chatId,
|
|
1103
|
+
dialogId: chat.dialogId,
|
|
1104
|
+
comments: chatResult.comments,
|
|
1105
|
+
errors: [...errors, ...chatResult.errors]
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1110
|
+
errors.push(`chat API: ${message}`);
|
|
1111
|
+
if (preferApi === 'chat') {
|
|
1112
|
+
return {
|
|
1113
|
+
taskId,
|
|
1114
|
+
apiMode: 'chat',
|
|
1115
|
+
chatId: chat.chatId,
|
|
1116
|
+
dialogId: chat.dialogId,
|
|
1117
|
+
comments: [],
|
|
1118
|
+
errors
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const legacyResult = await this.getTaskCommentsLegacy(taskId, options);
|
|
1124
|
+
return {
|
|
1125
|
+
taskId,
|
|
1126
|
+
apiMode: 'legacy',
|
|
1127
|
+
chatId: chat?.chatId,
|
|
1128
|
+
dialogId: chat?.dialogId,
|
|
1129
|
+
comments: legacyResult.comments,
|
|
1130
|
+
errors: [...errors, ...legacyResult.errors]
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
576
1133
|
// Project Methods (Bitrix24 workgroups)
|
|
577
1134
|
async createProject(project) {
|
|
578
1135
|
const result = await this.makeRequest('sonet_group.create', { fields: project });
|
|
@@ -735,6 +1292,7 @@ export class Bitrix24Client {
|
|
|
735
1292
|
tasks_access: false,
|
|
736
1293
|
projects_access: false,
|
|
737
1294
|
disk_access: false,
|
|
1295
|
+
chat_access: false,
|
|
738
1296
|
error_details: []
|
|
739
1297
|
};
|
|
740
1298
|
try {
|
|
@@ -757,6 +1315,9 @@ export class Bitrix24Client {
|
|
|
757
1315
|
if (scopes.includes('disk')) {
|
|
758
1316
|
results.disk_access = true;
|
|
759
1317
|
}
|
|
1318
|
+
if (scopes.includes('im')) {
|
|
1319
|
+
results.chat_access = true;
|
|
1320
|
+
}
|
|
760
1321
|
try {
|
|
761
1322
|
await this.getLatestTasks(1);
|
|
762
1323
|
results.tasks_access = true;
|
|
@@ -782,6 +1343,25 @@ export class Bitrix24Client {
|
|
|
782
1343
|
results.error_details.push(`disk access failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
783
1344
|
}
|
|
784
1345
|
}
|
|
1346
|
+
try {
|
|
1347
|
+
const latestTasks = await this.getLatestTasks(1);
|
|
1348
|
+
const sampleTaskId = latestTasks[0]?.ID;
|
|
1349
|
+
if (sampleTaskId) {
|
|
1350
|
+
const chat = await this.resolveTaskChat(String(sampleTaskId));
|
|
1351
|
+
if (chat) {
|
|
1352
|
+
await this.makeRequest('im.dialog.messages.get', {
|
|
1353
|
+
DIALOG_ID: chat.dialogId,
|
|
1354
|
+
LIMIT: 1
|
|
1355
|
+
});
|
|
1356
|
+
results.chat_access = true;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
catch (error) {
|
|
1361
|
+
if (!results.chat_access) {
|
|
1362
|
+
results.error_details.push(`chat access failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
785
1365
|
return results;
|
|
786
1366
|
}
|
|
787
1367
|
}
|