n8n-nodes-pragma-bitrix24 1.0.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/LICENSE +51 -0
- package/README.md +340 -0
- package/dist/credentials/Bitrix24Api.credentials.js +55 -0
- package/dist/nodes/Bitrix24/Bitrix24.node.js +1410 -0
- package/dist/nodes/Bitrix24/Company.js +17 -0
- package/dist/nodes/Bitrix24/Contact.js +31 -0
- package/dist/nodes/Bitrix24/Deal.js +17 -0
- package/dist/nodes/Bitrix24/Helpers.js +300 -0
- package/dist/nodes/Bitrix24/Lead.js +17 -0
- package/dist/nodes/Bitrix24/SmartProcess.js +1 -0
- package/dist/nodes/Bitrix24/bitrix24.svg +7 -0
- package/dist/nodes/Bitrix24/types.js +58 -0
- package/dist/nodes/Bitrix24AI/Bitrix24AI.node.js +205 -0
- package/dist/nodes/Bitrix24App/Bitrix24App.node.js +179 -0
- package/dist/nodes/Bitrix24Auxiliary/Bitrix24Auxiliary.node.js +566 -0
- package/dist/nodes/Bitrix24Booking/Bitrix24Booking.node.js +871 -0
- package/dist/nodes/Bitrix24Calendar/Bitrix24Calendar.node.js +471 -0
- package/dist/nodes/Bitrix24ChatBot/Bitrix24ChatBot.node.js +522 -0
- package/dist/nodes/Bitrix24Commerce/Bitrix24Commerce.node.js +431 -0
- package/dist/nodes/Bitrix24Department/Bitrix24Department.node.js +317 -0
- package/dist/nodes/Bitrix24Disk/Bitrix24Disk.node.js +334 -0
- package/dist/nodes/Bitrix24Document/Bitrix24Document.node.js +280 -0
- package/dist/nodes/Bitrix24Entity/Bitrix24Entity.node.js +263 -0
- package/dist/nodes/Bitrix24Group/Bitrix24Group.node.js +327 -0
- package/dist/nodes/Bitrix24Lists/Bitrix24Lists.node.js +406 -0
- package/dist/nodes/Bitrix24Log/Bitrix24Log.node.js +309 -0
- package/dist/nodes/Bitrix24Mail/Bitrix24Mail.node.js +109 -0
- package/dist/nodes/Bitrix24MessageService/Bitrix24MessageService.node.js +218 -0
- package/dist/nodes/Bitrix24OpenChannels/Bitrix24OpenChannels.node.js +379 -0
- package/dist/nodes/Bitrix24PaySystem/Bitrix24PaySystem.node.js +241 -0
- package/dist/nodes/Bitrix24Pipeline/Bitrix24Pipeline.node.js +553 -0
- package/dist/nodes/Bitrix24Pipeline/bitrix24.svg +7 -0
- package/dist/nodes/Bitrix24Sale/Bitrix24Sale.node.js +391 -0
- package/dist/nodes/Bitrix24Scrum/Bitrix24Scrum.node.js +555 -0
- package/dist/nodes/Bitrix24Scrum/bitrix24.svg +7 -0
- package/dist/nodes/Bitrix24Sign/Bitrix24Sign.node.js +132 -0
- package/dist/nodes/Bitrix24Social/Bitrix24Social.node.js +224 -0
- package/dist/nodes/Bitrix24Task/Bitrix24Task.node.js +444 -0
- package/dist/nodes/Bitrix24Telephony/Bitrix24Telephony.node.js +511 -0
- package/dist/nodes/Bitrix24Timeman/Bitrix24Timeman.node.js +196 -0
- package/dist/nodes/Bitrix24Tool/Bitrix24Tool.node.js +1035 -0
- package/dist/nodes/Bitrix24Tool/bitrix24.svg +7 -0
- package/dist/nodes/Bitrix24Trigger/Bitrix24Trigger.node.js +184 -0
- package/dist/nodes/Bitrix24User/Bitrix24User.node.js +351 -0
- package/dist/nodes/Bitrix24UserField/Bitrix24UserField.node.js +386 -0
- package/dist/nodes/Bitrix24UserField/bitrix24.svg +7 -0
- package/dist/nodes/shared/bitrix24.svg +7 -0
- package/dist/nodes/shared/localization.js +189 -0
- package/dist/nodes/shared/types.js +22 -0
- package/index.js +10 -0
- package/package.json +108 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Company = void 0;
|
|
4
|
+
class Company {
|
|
5
|
+
static getDescription() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'Company',
|
|
8
|
+
value: 'company',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
// Методы специфичные для Company
|
|
12
|
+
static getDefaultFields() {
|
|
13
|
+
return ['TITLE', 'COMPANY_TYPE', 'INDUSTRY', 'PHONE', 'EMAIL', 'WEB', 'OPENED', 'ASSIGNED_BY_ID'];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.Company = Company;
|
|
17
|
+
Company.resource = 'company';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Contact = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
class Contact {
|
|
6
|
+
static getDescription() {
|
|
7
|
+
return {
|
|
8
|
+
name: 'Contact',
|
|
9
|
+
value: 'contact',
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
// Методы специфичные для Contact
|
|
13
|
+
static getDefaultFields() {
|
|
14
|
+
return ['NAME', 'SECOND_NAME', 'LAST_NAME', 'PHONE', 'EMAIL', 'COMPANY_ID', 'OPENED', 'ASSIGNED_BY_ID', 'TYPE_ID'];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.Contact = Contact;
|
|
18
|
+
Contact.resource = 'contact';
|
|
19
|
+
// Типы значений для полей контакта
|
|
20
|
+
Contact.communicationTypes = [
|
|
21
|
+
{ name: 'Work', value: types_1.CommunicationType.WORK },
|
|
22
|
+
{ name: 'Mobile', value: types_1.CommunicationType.MOBILE },
|
|
23
|
+
{ name: 'Home', value: types_1.CommunicationType.HOME },
|
|
24
|
+
{ name: 'Fax', value: types_1.CommunicationType.FAX },
|
|
25
|
+
{ name: 'Telegram', value: types_1.CommunicationType.TELEGRAM },
|
|
26
|
+
{ name: 'WhatsApp', value: types_1.CommunicationType.WHATSAPP },
|
|
27
|
+
{ name: 'Viber', value: types_1.CommunicationType.VIBER },
|
|
28
|
+
{ name: 'Facebook', value: types_1.CommunicationType.FACEBOOK },
|
|
29
|
+
{ name: 'Skype', value: types_1.CommunicationType.SKYPE },
|
|
30
|
+
{ name: 'Other', value: types_1.CommunicationType.OTHER },
|
|
31
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Deal = void 0;
|
|
4
|
+
class Deal {
|
|
5
|
+
static getDescription() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'Deal',
|
|
8
|
+
value: 'deal',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
// Методы специфичные для Deal
|
|
12
|
+
static getDefaultFields() {
|
|
13
|
+
return ['TITLE', 'TYPE_ID', 'STAGE_ID', 'OPPORTUNITY', 'CURRENCY_ID', 'COMPANY_ID', 'CONTACT_ID', 'OPENED', 'ASSIGNED_BY_ID'];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.Deal = Deal;
|
|
17
|
+
Deal.resource = 'deal';
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BitrixSuccessSchema = exports.BitrixErrorSchema = void 0;
|
|
4
|
+
exports.sleep = sleep;
|
|
5
|
+
exports.safeJsonParse = safeJsonParse;
|
|
6
|
+
exports.bitrixRequestWithRetry = bitrixRequestWithRetry;
|
|
7
|
+
exports.bitrixBatchRequest = bitrixBatchRequest;
|
|
8
|
+
exports.prepareBatchCommands = prepareBatchCommands;
|
|
9
|
+
exports.bitrixFetchAll = bitrixFetchAll;
|
|
10
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
minDelayMs: 100, // 10 req/sec for paid plans (use 500 for free)
|
|
14
|
+
maxDelayMs: 10000, // 10 seconds max
|
|
15
|
+
maxRetries: 5,
|
|
16
|
+
circuitBreakerThreshold: 5,
|
|
17
|
+
circuitBreakerTimeoutMs: 30000,
|
|
18
|
+
};
|
|
19
|
+
// ============================================
|
|
20
|
+
// Zod Schemas for Runtime Validation
|
|
21
|
+
// ============================================
|
|
22
|
+
exports.BitrixErrorSchema = zod_1.z.object({
|
|
23
|
+
error: zod_1.z.string().optional(),
|
|
24
|
+
error_description: zod_1.z.string().optional(),
|
|
25
|
+
result: zod_1.z.unknown().optional(),
|
|
26
|
+
});
|
|
27
|
+
exports.BitrixSuccessSchema = zod_1.z.object({
|
|
28
|
+
result: zod_1.z.unknown(),
|
|
29
|
+
time: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
30
|
+
total: zod_1.z.number().optional(),
|
|
31
|
+
next: zod_1.z.number().optional(),
|
|
32
|
+
});
|
|
33
|
+
// ============================================
|
|
34
|
+
// Utility Functions
|
|
35
|
+
// ============================================
|
|
36
|
+
/**
|
|
37
|
+
* Sleep helper for delays
|
|
38
|
+
*/
|
|
39
|
+
async function sleep(ms) {
|
|
40
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Safely parse JSON with error handling
|
|
44
|
+
* @param jsonString - Raw JSON string
|
|
45
|
+
* @param fieldName - Field name for error messages
|
|
46
|
+
*/
|
|
47
|
+
function safeJsonParse(jsonString, fieldName = 'JSON') {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(jsonString);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new Error(`Invalid ${fieldName}: ${error.message}. Please check the JSON syntax.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const circuitBreakerMap = new Map();
|
|
56
|
+
function getCircuitBreaker(workflowId) {
|
|
57
|
+
if (!circuitBreakerMap.has(workflowId)) {
|
|
58
|
+
circuitBreakerMap.set(workflowId, { failures: 0, lastFailure: 0, isOpen: false });
|
|
59
|
+
}
|
|
60
|
+
return circuitBreakerMap.get(workflowId);
|
|
61
|
+
}
|
|
62
|
+
function resetCircuitBreaker(workflowId) {
|
|
63
|
+
const breaker = getCircuitBreaker(workflowId);
|
|
64
|
+
breaker.failures = 0;
|
|
65
|
+
breaker.isOpen = false;
|
|
66
|
+
}
|
|
67
|
+
function recordFailure(workflowId, config) {
|
|
68
|
+
const breaker = getCircuitBreaker(workflowId);
|
|
69
|
+
breaker.failures++;
|
|
70
|
+
breaker.lastFailure = Date.now();
|
|
71
|
+
if (breaker.failures >= config.circuitBreakerThreshold) {
|
|
72
|
+
breaker.isOpen = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const rateLimiterMap = new Map();
|
|
76
|
+
function getRateLimiter(domain, config) {
|
|
77
|
+
if (!rateLimiterMap.has(domain)) {
|
|
78
|
+
rateLimiterMap.set(domain, { lastRequestTime: 0, currentDelay: config.minDelayMs });
|
|
79
|
+
}
|
|
80
|
+
return rateLimiterMap.get(domain);
|
|
81
|
+
}
|
|
82
|
+
async function waitForRateLimit(domain, config) {
|
|
83
|
+
const limiter = getRateLimiter(domain, config);
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const timeSinceLastRequest = now - limiter.lastRequestTime;
|
|
86
|
+
if (timeSinceLastRequest < limiter.currentDelay) {
|
|
87
|
+
await sleep(limiter.currentDelay - timeSinceLastRequest);
|
|
88
|
+
}
|
|
89
|
+
limiter.lastRequestTime = Date.now();
|
|
90
|
+
}
|
|
91
|
+
function updateRateLimitFromHeader(domain, retryAfter, config) {
|
|
92
|
+
const limiter = getRateLimiter(domain, config);
|
|
93
|
+
if (retryAfter) {
|
|
94
|
+
// Server told us to wait, increase delay
|
|
95
|
+
limiter.currentDelay = Math.min(retryAfter * 1000, config.maxDelayMs);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Success, gradually reduce delay
|
|
99
|
+
limiter.currentDelay = Math.max(limiter.currentDelay * 0.9, config.minDelayMs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ============================================
|
|
103
|
+
// Main Request Function
|
|
104
|
+
// ============================================
|
|
105
|
+
/**
|
|
106
|
+
* Enhanced Bitrix24 API request with:
|
|
107
|
+
* - Adaptive rate limiting
|
|
108
|
+
* - Per-workflow circuit breaker
|
|
109
|
+
* - Exponential backoff
|
|
110
|
+
* - Rich error messages
|
|
111
|
+
*/
|
|
112
|
+
async function bitrixRequestWithRetry(method, endpoint, body = {}, qs = {}, overrideAuth, config = {}) {
|
|
113
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
114
|
+
const workflowId = this.getWorkflow().id?.toString() || 'default';
|
|
115
|
+
// Get domain for rate limiting
|
|
116
|
+
let domain = '';
|
|
117
|
+
let url = '';
|
|
118
|
+
if (overrideAuth) {
|
|
119
|
+
domain = overrideAuth.domain;
|
|
120
|
+
url = `https://${overrideAuth.domain}/rest/${endpoint}`;
|
|
121
|
+
qs.auth = overrideAuth.accessToken;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const credentials = await this.getCredentials('bitrix24Api');
|
|
125
|
+
if (!credentials) {
|
|
126
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No credentials returned!');
|
|
127
|
+
}
|
|
128
|
+
const webhookUrl = credentials.webhookUrl;
|
|
129
|
+
// Extract domain from webhook URL
|
|
130
|
+
const domainMatch = webhookUrl.match(/https?:\/\/([^/]+)/);
|
|
131
|
+
domain = domainMatch ? domainMatch[1] : 'unknown';
|
|
132
|
+
url = `${webhookUrl}${endpoint}`;
|
|
133
|
+
}
|
|
134
|
+
// Circuit Breaker Check
|
|
135
|
+
const breaker = getCircuitBreaker(workflowId);
|
|
136
|
+
if (breaker.isOpen) {
|
|
137
|
+
const timeSinceLastFailure = Date.now() - breaker.lastFailure;
|
|
138
|
+
if (timeSinceLastFailure < finalConfig.circuitBreakerTimeoutMs) {
|
|
139
|
+
const waitTime = Math.ceil((finalConfig.circuitBreakerTimeoutMs - timeSinceLastFailure) / 1000);
|
|
140
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Circuit Breaker Active: Bitrix24 API has too many failures. Wait ${waitTime}s or check API status.`, { description: 'The circuit breaker opens after 5 consecutive failures to prevent cascading errors.' });
|
|
141
|
+
}
|
|
142
|
+
// Reset if timeout passed
|
|
143
|
+
resetCircuitBreaker(workflowId);
|
|
144
|
+
}
|
|
145
|
+
let attempt = 0;
|
|
146
|
+
let delay = finalConfig.minDelayMs;
|
|
147
|
+
while (attempt < finalConfig.maxRetries) {
|
|
148
|
+
try {
|
|
149
|
+
// Wait for rate limit
|
|
150
|
+
await waitForRateLimit(domain, finalConfig);
|
|
151
|
+
const options = {
|
|
152
|
+
method,
|
|
153
|
+
url,
|
|
154
|
+
body,
|
|
155
|
+
qs,
|
|
156
|
+
json: true,
|
|
157
|
+
returnFullResponse: true, // Get headers for Retry-After
|
|
158
|
+
};
|
|
159
|
+
const response = await this.helpers.httpRequest(options);
|
|
160
|
+
const responseBody = response.body;
|
|
161
|
+
const headers = response.headers;
|
|
162
|
+
// Check for Retry-After header
|
|
163
|
+
const retryAfter = headers['retry-after'] ? parseInt(headers['retry-after'], 10) : undefined;
|
|
164
|
+
updateRateLimitFromHeader(domain, retryAfter, finalConfig);
|
|
165
|
+
// Runtime Validation
|
|
166
|
+
const parsed = exports.BitrixSuccessSchema.safeParse(responseBody);
|
|
167
|
+
if (!parsed.success) {
|
|
168
|
+
const errorParsed = exports.BitrixErrorSchema.safeParse(responseBody);
|
|
169
|
+
if (errorParsed.success && errorParsed.data.error) {
|
|
170
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Bitrix24 API Error: ${errorParsed.data.error_description || errorParsed.data.error}`, { description: getErrorSuggestion(errorParsed.data.error_description || errorParsed.data.error) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Success! Reset breaker
|
|
174
|
+
resetCircuitBreaker(workflowId);
|
|
175
|
+
return responseBody;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const err = error;
|
|
179
|
+
// Handle Rate Limiting (429) and Server Errors (5xx)
|
|
180
|
+
const status = err.response?.status;
|
|
181
|
+
if (status === 429 || (status && status >= 500 && status < 600)) {
|
|
182
|
+
attempt++;
|
|
183
|
+
// Check for Retry-After header on 429
|
|
184
|
+
const retryAfter = err.response?.headers?.['retry-after'];
|
|
185
|
+
if (retryAfter) {
|
|
186
|
+
delay = parseInt(retryAfter, 10) * 1000;
|
|
187
|
+
updateRateLimitFromHeader(domain, parseInt(retryAfter, 10), finalConfig);
|
|
188
|
+
}
|
|
189
|
+
if (attempt >= finalConfig.maxRetries) {
|
|
190
|
+
recordFailure(workflowId, finalConfig);
|
|
191
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Bitrix24 API Request Failed after ${finalConfig.maxRetries} attempts: ${err.message}`, { description: 'Consider reducing request frequency or upgrading your Bitrix24 plan.' });
|
|
192
|
+
}
|
|
193
|
+
await sleep(delay);
|
|
194
|
+
delay = Math.min(delay * 2, finalConfig.maxDelayMs); // Exponential backoff
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// For other errors, throw immediately with helpful message
|
|
198
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), formatErrorMessage(err), { description: getErrorSuggestion(err.response?.data?.error_description || err.message) });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Should never reach here, but TypeScript needs it
|
|
202
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Unexpected error in Bitrix24 request handler');
|
|
203
|
+
}
|
|
204
|
+
// ============================================
|
|
205
|
+
// Batch API Support
|
|
206
|
+
// ============================================
|
|
207
|
+
/**
|
|
208
|
+
* Execute multiple API calls in a single batch request
|
|
209
|
+
* More efficient than multiple individual requests
|
|
210
|
+
*/
|
|
211
|
+
async function bitrixBatchRequest(commands, haltOnError = false) {
|
|
212
|
+
return bitrixRequestWithRetry.call(this, 'POST', 'batch.json', {
|
|
213
|
+
halt: haltOnError ? 1 : 0,
|
|
214
|
+
cmd: commands,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Prepare batch commands for CRM operations
|
|
219
|
+
*/
|
|
220
|
+
function prepareBatchCommands(resource, method, items, prefix = 'cmd') {
|
|
221
|
+
const commands = {};
|
|
222
|
+
items.forEach((item, index) => {
|
|
223
|
+
const params = new URLSearchParams();
|
|
224
|
+
Object.entries(item).forEach(([key, value]) => {
|
|
225
|
+
if (typeof value === 'object') {
|
|
226
|
+
params.append(key, JSON.stringify(value));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
params.append(key, String(value));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
commands[`${prefix}_${index}`] = `${resource}.${method}?${params.toString()}`;
|
|
233
|
+
});
|
|
234
|
+
return commands;
|
|
235
|
+
}
|
|
236
|
+
// ============================================
|
|
237
|
+
// Error Handling Helpers
|
|
238
|
+
// ============================================
|
|
239
|
+
function formatErrorMessage(err) {
|
|
240
|
+
const apiError = err.response?.data;
|
|
241
|
+
if (apiError?.error_description) {
|
|
242
|
+
return `Bitrix24 Error: ${apiError.error_description}`;
|
|
243
|
+
}
|
|
244
|
+
if (apiError?.error) {
|
|
245
|
+
return `Bitrix24 Error: ${apiError.error}`;
|
|
246
|
+
}
|
|
247
|
+
return err.message;
|
|
248
|
+
}
|
|
249
|
+
function getErrorSuggestion(errorText) {
|
|
250
|
+
const suggestions = {
|
|
251
|
+
'ACCESS_DENIED': 'Your webhook or user lacks permissions for this action. Check scope settings in Bitrix24.',
|
|
252
|
+
'limit': 'You hit the API rate limit. Consider upgrading your Bitrix24 plan or reducing request frequency.',
|
|
253
|
+
'NOT_FOUND': 'The requested entity does not exist. Verify the ID is correct.',
|
|
254
|
+
'INVALID_CREDENTIALS': 'Your webhook URL is invalid or expired. Generate a new one in Bitrix24.',
|
|
255
|
+
'INVALID_REQUEST': 'The request parameters are incorrect. Check required fields.',
|
|
256
|
+
'METHOD_NOT_FOUND': 'This API method does not exist or is not available for your plan.',
|
|
257
|
+
'NO_AUTH_FOUND': 'Authentication failed. Verify your webhook URL includes a valid token.',
|
|
258
|
+
'QUERY_LIMIT_EXCEEDED': 'Too many items in query. Use pagination or reduce filter scope.',
|
|
259
|
+
'EXPIRED_TOKEN': 'OAuth token has expired. Reconnect your Bitrix24 integration.',
|
|
260
|
+
};
|
|
261
|
+
for (const [key, suggestion] of Object.entries(suggestions)) {
|
|
262
|
+
if (errorText.toLowerCase().includes(key.toLowerCase())) {
|
|
263
|
+
return suggestion;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return 'Check Bitrix24 API documentation for more details on this error.';
|
|
267
|
+
}
|
|
268
|
+
// ============================================
|
|
269
|
+
// Pagination Helpers
|
|
270
|
+
// ============================================
|
|
271
|
+
/**
|
|
272
|
+
* Fetch all pages of a paginated response
|
|
273
|
+
* Includes safety limit to prevent memory issues
|
|
274
|
+
*/
|
|
275
|
+
async function bitrixFetchAll(method, endpoint, params = {}, maxPages = 100) {
|
|
276
|
+
const allResults = [];
|
|
277
|
+
let start = 0;
|
|
278
|
+
let page = 0;
|
|
279
|
+
while (page < maxPages) {
|
|
280
|
+
const response = await bitrixRequestWithRetry.call(this, method, endpoint, {
|
|
281
|
+
...params,
|
|
282
|
+
start,
|
|
283
|
+
});
|
|
284
|
+
const results = response.result;
|
|
285
|
+
if (results && Array.isArray(results)) {
|
|
286
|
+
allResults.push(...results);
|
|
287
|
+
}
|
|
288
|
+
// Check if there are more pages
|
|
289
|
+
if (response.next === undefined || response.next === null) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
start = response.next;
|
|
293
|
+
page++;
|
|
294
|
+
}
|
|
295
|
+
if (page >= maxPages) {
|
|
296
|
+
// Warn about pagination limit
|
|
297
|
+
this.logger?.warn?.(`Bitrix24: Stopped after ${maxPages} pages to prevent memory issues. Total fetched: ${allResults.length}`);
|
|
298
|
+
}
|
|
299
|
+
return allResults;
|
|
300
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Lead = void 0;
|
|
4
|
+
class Lead {
|
|
5
|
+
static getDescription() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'Lead',
|
|
8
|
+
value: 'lead',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
// Методы специфичные для Lead
|
|
12
|
+
static getDefaultFields() {
|
|
13
|
+
return ['TITLE', 'NAME', 'SECOND_NAME', 'LAST_NAME', 'STATUS_ID', 'OPENED', 'ASSIGNED_BY_ID', 'PHONE', 'EMAIL'];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.Lead = Lead;
|
|
17
|
+
Lead.resource = 'lead';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
4
|
+
<path d="M16,0 C24.8365,0 32,7.1635 32,16 C32,24.8365 24.8365,32 16,32 C7.1635,32 0,24.8365 0,16 C0,7.1635 7.1635,0 16,0" fill="#2FC7F7"/>
|
|
5
|
+
<path d="M13.3333,8 L18.6667,8 C21.6122,8 24,10.3878 24,13.3333 L24,18.6667 C24,21.6122 21.6122,24 18.6667,24 L13.3333,24 C10.3878,24 8,21.6122 8,18.6667 L8,13.3333 C8,10.3878 10.3878,8 13.3333,8" fill="#FFFFFF"/>
|
|
6
|
+
</g>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sleep = exports.processFormFields = exports.formatCommunicationField = exports.formatContactCommunicationField = exports.CommunicationType = void 0;
|
|
4
|
+
var CommunicationType;
|
|
5
|
+
(function (CommunicationType) {
|
|
6
|
+
CommunicationType["WORK"] = "WORK";
|
|
7
|
+
CommunicationType["MOBILE"] = "MOBILE";
|
|
8
|
+
CommunicationType["HOME"] = "HOME";
|
|
9
|
+
CommunicationType["FAX"] = "FAX";
|
|
10
|
+
CommunicationType["OTHER"] = "OTHER";
|
|
11
|
+
CommunicationType["TELEGRAM"] = "TELEGRAM";
|
|
12
|
+
CommunicationType["WHATSAPP"] = "WHATSAPP";
|
|
13
|
+
CommunicationType["VIBER"] = "VIBER";
|
|
14
|
+
CommunicationType["FACEBOOK"] = "FACEBOOK";
|
|
15
|
+
CommunicationType["SKYPE"] = "SKYPE";
|
|
16
|
+
})(CommunicationType || (exports.CommunicationType = CommunicationType = {}));
|
|
17
|
+
// Формат для коммуникационных полей в сущности Contact
|
|
18
|
+
const formatContactCommunicationField = (value, type = CommunicationType.WORK) => {
|
|
19
|
+
return [{ VALUE: value, VALUE_TYPE: type }];
|
|
20
|
+
};
|
|
21
|
+
exports.formatContactCommunicationField = formatContactCommunicationField;
|
|
22
|
+
// Формат для коммуникационных полей для всех других сущностей
|
|
23
|
+
const formatCommunicationField = (value, type = CommunicationType.WORK) => {
|
|
24
|
+
return [{ VALUE: value, TYPE: type }];
|
|
25
|
+
};
|
|
26
|
+
exports.formatCommunicationField = formatCommunicationField;
|
|
27
|
+
const processFormFields = (fieldsCollection) => {
|
|
28
|
+
const fields = {};
|
|
29
|
+
for (const field of fieldsCollection) {
|
|
30
|
+
// Проверяем, является ли это полем связи (PHONE, EMAIL, IM, WEB)
|
|
31
|
+
if (['PHONE', 'EMAIL', 'IM', 'WEB'].some(type => field.fieldName.includes(type))) {
|
|
32
|
+
const valueType = field.fieldValueType || CommunicationType.WORK;
|
|
33
|
+
// Для полей контакта нужна специальная обработка
|
|
34
|
+
if (field.resource === 'contact') {
|
|
35
|
+
// Используем специальный формат для контактов
|
|
36
|
+
fields[field.fieldName] = (0, exports.formatContactCommunicationField)(field.fieldValue, valueType);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Для других сущностей используем стандартный формат
|
|
40
|
+
fields[field.fieldName] = (0, exports.formatCommunicationField)(field.fieldValue, valueType);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (field.field?.type === 'enumeration' && field.field.items && Array.isArray(field.field.items)) {
|
|
44
|
+
const selectedOption = field.field.items.find(item => item.VALUE === field.fieldValue);
|
|
45
|
+
if (selectedOption) {
|
|
46
|
+
fields[field.fieldName] = selectedOption.ID;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
fields[field.fieldName] = field.fieldValue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return fields;
|
|
54
|
+
};
|
|
55
|
+
exports.processFormFields = processFormFields;
|
|
56
|
+
// Helper function for rate limiting
|
|
57
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
58
|
+
exports.sleep = sleep;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bitrix24 AI — Нода управления AI-движками
|
|
4
|
+
*
|
|
5
|
+
* Bitrix24 PRAGMA.by nodes for n8n
|
|
6
|
+
* Профессиональная интеграция с Битрикс24.
|
|
7
|
+
*
|
|
8
|
+
* @author PRAGMA & Азбука Решений
|
|
9
|
+
* @copyright 2026 PRAGMA (https://pragma.by/) & Азбука Решений (https://abc-solution.ru/)
|
|
10
|
+
*
|
|
11
|
+
* Контакты:
|
|
12
|
+
* 🇧🇾 PRAGMA: +375 (44) 702-70-90 | https://pragma.by/
|
|
13
|
+
* 🇷🇺 Азбука Решений: +7 (939) 555-19-60 | https://abc-solution.ru/
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.Bitrix24AI = void 0;
|
|
17
|
+
const Helpers_1 = require("../Bitrix24/Helpers");
|
|
18
|
+
class Bitrix24AI {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.description = {
|
|
21
|
+
displayName: 'Bitrix24 AI Копилот',
|
|
22
|
+
name: 'bitrix24Ai',
|
|
23
|
+
icon: 'file:bitrix24.svg',
|
|
24
|
+
group: ['transform'],
|
|
25
|
+
version: 1,
|
|
26
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
27
|
+
description: 'Управление AI-движками и промптами в Битрикс24 CoPilot',
|
|
28
|
+
defaults: {
|
|
29
|
+
name: 'Bitrix24 AI Копилот',
|
|
30
|
+
},
|
|
31
|
+
inputs: ['main'],
|
|
32
|
+
outputs: ['main'],
|
|
33
|
+
credentials: [
|
|
34
|
+
{
|
|
35
|
+
name: 'bitrix24Api',
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
properties: [
|
|
40
|
+
{
|
|
41
|
+
displayName: 'Resource',
|
|
42
|
+
name: 'resource',
|
|
43
|
+
type: 'options',
|
|
44
|
+
noDataExpression: true,
|
|
45
|
+
options: [
|
|
46
|
+
{
|
|
47
|
+
name: 'Engine',
|
|
48
|
+
value: 'engine',
|
|
49
|
+
description: 'Manage AI Engines',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Prompt',
|
|
53
|
+
value: 'prompt',
|
|
54
|
+
description: 'Manage AI Prompts',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
default: 'engine',
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
displayName: 'Operation',
|
|
62
|
+
name: 'operation',
|
|
63
|
+
type: 'options',
|
|
64
|
+
noDataExpression: true,
|
|
65
|
+
options: [
|
|
66
|
+
// Engine
|
|
67
|
+
{
|
|
68
|
+
name: 'Register',
|
|
69
|
+
value: 'register',
|
|
70
|
+
action: 'Register new AI Engine',
|
|
71
|
+
displayOptions: { show: { resource: ['engine'] } },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Unregister',
|
|
75
|
+
value: 'unregister',
|
|
76
|
+
action: 'Delete AI Engine',
|
|
77
|
+
displayOptions: { show: { resource: ['engine'] } },
|
|
78
|
+
},
|
|
79
|
+
// Prompt
|
|
80
|
+
{
|
|
81
|
+
name: 'Register',
|
|
82
|
+
value: 'register',
|
|
83
|
+
action: 'Register new Prompt',
|
|
84
|
+
displayOptions: { show: { resource: ['prompt'] } },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Unregister',
|
|
88
|
+
value: 'unregister',
|
|
89
|
+
action: 'Delete Prompt',
|
|
90
|
+
displayOptions: { show: { resource: ['prompt'] } },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
default: 'register',
|
|
94
|
+
required: true,
|
|
95
|
+
},
|
|
96
|
+
// Engine Fields
|
|
97
|
+
{
|
|
98
|
+
displayName: 'Code',
|
|
99
|
+
name: 'code',
|
|
100
|
+
type: 'string',
|
|
101
|
+
default: 'n8n_ai',
|
|
102
|
+
displayOptions: { show: { resource: ['engine'], operation: ['register', 'unregister'] } },
|
|
103
|
+
description: 'Unique code for the engine',
|
|
104
|
+
required: true,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
displayName: 'Category',
|
|
108
|
+
name: 'category',
|
|
109
|
+
type: 'options',
|
|
110
|
+
options: [
|
|
111
|
+
{ name: 'Text', value: 'text' },
|
|
112
|
+
{ name: 'Image', value: 'image' },
|
|
113
|
+
{ name: 'Audio', value: 'audio' },
|
|
114
|
+
],
|
|
115
|
+
default: 'text',
|
|
116
|
+
displayOptions: { show: { resource: ['engine'], operation: ['register'] } },
|
|
117
|
+
required: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
displayName: 'Handler URL',
|
|
121
|
+
name: 'handlerUrl',
|
|
122
|
+
type: 'string',
|
|
123
|
+
default: '',
|
|
124
|
+
displayOptions: { show: { resource: ['engine'], operation: ['register'] } },
|
|
125
|
+
description: 'Webhook URL of n8n workflow to handle completions',
|
|
126
|
+
required: true,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
displayName: 'Name',
|
|
130
|
+
name: 'name',
|
|
131
|
+
type: 'string',
|
|
132
|
+
default: 'n8n AI Model',
|
|
133
|
+
displayOptions: { show: { resource: ['engine'], operation: ['register'] } },
|
|
134
|
+
},
|
|
135
|
+
// Prompt Fields
|
|
136
|
+
{
|
|
137
|
+
displayName: 'Prompt Code',
|
|
138
|
+
name: 'promptCode',
|
|
139
|
+
type: 'string',
|
|
140
|
+
default: '',
|
|
141
|
+
displayOptions: { show: { resource: ['prompt'], operation: ['register', 'unregister'] } },
|
|
142
|
+
required: true,
|
|
143
|
+
},
|
|
144
|
+
// Note: Prompt registration is complex, usually requires localizations etc.
|
|
145
|
+
// Simplified for now assuming basic usage.
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async execute() {
|
|
150
|
+
const items = this.getInputData();
|
|
151
|
+
const returnData = [];
|
|
152
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
153
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
154
|
+
for (let i = 0; i < items.length; i++) {
|
|
155
|
+
try {
|
|
156
|
+
let endpoint = '';
|
|
157
|
+
let body = {};
|
|
158
|
+
if (resource === 'engine') {
|
|
159
|
+
if (operation === 'register') {
|
|
160
|
+
endpoint = 'ai.engine.register';
|
|
161
|
+
body.CODE = this.getNodeParameter('code', i);
|
|
162
|
+
body.CATEGORY = this.getNodeParameter('category', i);
|
|
163
|
+
body.COM_HANDLER = this.getNodeParameter('handlerUrl', i);
|
|
164
|
+
// Name implies Localization settings (more complex)
|
|
165
|
+
// For now we assume user will manually config or simple pass
|
|
166
|
+
// Actually AI engine registration is tricky.
|
|
167
|
+
}
|
|
168
|
+
else if (operation === 'unregister') {
|
|
169
|
+
endpoint = 'ai.engine.unregister';
|
|
170
|
+
body.CODE = this.getNodeParameter('code', i);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (resource === 'prompt') {
|
|
174
|
+
if (operation === 'register') {
|
|
175
|
+
endpoint = 'ai.prompt.register';
|
|
176
|
+
body.CODE = this.getNodeParameter('promptCode', i);
|
|
177
|
+
}
|
|
178
|
+
else if (operation === 'unregister') {
|
|
179
|
+
endpoint = 'ai.prompt.unregister';
|
|
180
|
+
body.CODE = this.getNodeParameter('promptCode', i);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const response = await Helpers_1.bitrixRequestWithRetry.call(this, 'POST', endpoint, body);
|
|
184
|
+
let result = response.result;
|
|
185
|
+
if (Array.isArray(result)) {
|
|
186
|
+
for (const item of result) {
|
|
187
|
+
returnData.push({ json: item, pairedItem: { item: i } });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
returnData.push({ json: result, pairedItem: { item: i } });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (this.continueOnFail()) {
|
|
196
|
+
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [returnData];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.Bitrix24AI = Bitrix24AI;
|