/** * Automation worker code as a CommonJS string. * * Passed to `new Worker(AUTOMATION_WORKER_CODE, { eval: true })` so that the * worker runs in any environment (dev with tsx, production dist/) without * requiring a separate file to be present on disk. * * The source-of-truth logic lives here. esbuild inlines this string into the * bundle when the server is built for production. */ export const AUTOMATION_WORKER_CODE = ` 'use strict'; const { workerData, parentPort } = require('node:worker_threads'); const vm = require('vm'); if (!parentPort) throw new Error('Must be run as a Worker Thread'); const { code, organizationId, triggerData } = workerData; const logs = []; let callId = 0; const pending = new Map(); parentPort.on('message', function(msg) { if (msg.type !== 'storage-response') return; const p = pending.get(msg.callId); if (!p) return; pending.delete(msg.callId); if (msg.error !== undefined) { p.reject(new Error(msg.error)); } else { p.resolve(msg.result); } }); function callStorage(method, args) { return new Promise(function(resolve, reject) { const id = ++callId; pending.set(id, { resolve, reject }); parentPort.postMessage({ type: 'storage-call', callId: id, method, args }); }); } const ctx = { organizationId, triggerData, result: undefined, log: function() { const args = Array.prototype.slice.call(arguments); logs.push(args.map(function(a) { return typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a); }).join(' ')); }, forms: { list: function() { return callStorage('getFormsByOrganization', [organizationId]); }, get: function(formId) { return callStorage('getForm', [formId, organizationId]); }, getFields: function(formId) { return callStorage('getFormFields', [formId, organizationId]); }, getStatuses: function(formId) { return callStorage('getFormStatuses', [formId, organizationId]); }, }, users: { get: function(userId) { return callStorage('getUser', [userId]); }, list: function() { return callStorage('getUsersByOrganization', [organizationId]); }, update: function(userId, updates) { return callStorage('updateUser', [userId, updates]); }, updateStatus: function(userId, statusId) { return callStorage('updateUser', [userId, { statusId }]); }, setFieldValue: function(userId, fieldCodeOrId, value) { // taskId подставляем из данных триггера, чтобы оценки history-number были привязаны к задаче const taskId = triggerData && typeof triggerData === 'object' && typeof triggerData.taskId === 'number' ? triggerData.taskId : null; return callStorage('setUserProfileFieldValueFromAutomation', [userId, fieldCodeOrId, value, organizationId, taskId]); }, recalcFieldAverage: function(userId, fieldCodeOrId) { return callStorage('recalcUserFieldAverageFromAutomation', [userId, fieldCodeOrId, organizationId]); }, }, tasks: { list: function(formId) { return callStorage('getTasksByForm', [formId, organizationId]); }, get: function(taskId) { return callStorage('getTask', [taskId, organizationId]); }, create: function(data) { return callStorage('createTask', [Object.assign({}, data, { organizationId })]); }, update: function(taskId, updates) { return callStorage('updateTask', [taskId, organizationId, updates]); }, delete: function(taskId) { return callStorage('deleteTask', [taskId, organizationId]); }, getFieldValues: function(taskId) { return callStorage('getTaskFieldValues', [taskId, organizationId]); }, getAssignees: function(taskId) { return callStorage('getTaskAssignees', [taskId, organizationId]); }, setFieldValue: async function(taskId, fieldCode, value) { const fields = await callStorage('getTaskFieldValues', [taskId, organizationId]); const task = await callStorage('getTask', [taskId, organizationId]); if (!task) throw new Error('Task ' + taskId + ' not found'); const formFields = await callStorage('getFormFields', [task.formId, organizationId]); const field = formFields.find(function(f) { return (f.fieldCode || f.code) === fieldCode; }); if (!field) throw new Error('Field ' + fieldCode + ' not found'); const existing = fields.find(function(v) { return v.fieldId === field.id; }); if (existing) { return callStorage('updateTaskFieldValue', [taskId, field.id, organizationId, { value: String(value) }]); } else { return callStorage('createTaskFieldValue', [{ taskId, fieldId: field.id, formId: task.formId, value: String(value) }]); } }, // Сообщение в чат задачи от имени «Система» (messageType 'status_change', без уведомлений) sendMessage: function(taskId, message) { return callStorage('sendTaskMessageFromAutomation', [taskId, organizationId, String(message)]); }, // Отложенная смена статуса: воркер переведёт задачу в targetStatusId через delayMs, // если она к тому моменту останется в текущем статусе scheduleStatusChange: function(taskId, targetStatusId, delayMs) { return callStorage('createDelayedStatusChangeFromAutomation', [taskId, organizationId, Number(targetStatusId), Number(delayMs)]); }, }, }; async function run() { try { const wrappedCode = '(async () => { ' + code + ' })()'; const sandbox = vm.createContext({ ctx, console: { log: ctx.log, error: ctx.log, warn: ctx.log }, }); const script = new vm.Script(wrappedCode); await script.runInContext(sandbox, { timeout: 5000 }); parentPort.postMessage({ type: 'done', logs, result: ctx.result }); } catch (err) { const message = err && err.message ? err.message : String(err); logs.push('ERROR: ' + message); parentPort.postMessage({ type: 'done', logs, error: message }); } } run(); `;