@mspcopilot/n8n-nodes-connectwise 0.2.3 → 0.2.4
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/dist-community/credentials/ConnectWisePsaApi.credentials.js +1 -1
- package/dist-community/nodes/ConnectWise/PSA/ConnectWisePsa.node.js +1 -1
- package/dist-community/nodes/ConnectWise/PSA/helpers/field-collector.js +10 -1
- package/dist-community/nodes/ConnectWise/PSA/schema/resources/alert-consolidation.js +410 -3
- package/dist-community/nodes/ConnectWise/PSA/schema/resources/ticket.schema.js +5 -5
- package/dist-community/nodes/ConnectWise/PSA/schema/resources/timeEntry.schema.js +7 -0
- package/dist-community/nodes/ConnectWise/PSA/transport/version-check.js +1 -1
- package/package.json +1 -1
|
@@ -117,7 +117,7 @@ class ConnectWisePsaApi {
|
|
|
117
117
|
name: "checkForUpdates",
|
|
118
118
|
type: "boolean",
|
|
119
119
|
default: !0,
|
|
120
|
-
hint: "Current version: 0.2.
|
|
120
|
+
hint: "Current version: 0.2.4 (community edition)",
|
|
121
121
|
description: "Check for new versions of this node package and show notifications when updates are available"
|
|
122
122
|
} ];
|
|
123
123
|
async authenticate(credentials, requestOptions) {
|
|
@@ -74,7 +74,7 @@ const resources = {
|
|
|
74
74
|
|
|
75
75
|
class ConnectWisePsa {
|
|
76
76
|
constructor() {
|
|
77
|
-
console.log(`🔌 ConnectWise PSA Node Loaded | 📦 v0.2.
|
|
77
|
+
console.log(`🔌 ConnectWise PSA Node Loaded | 📦 v0.2.4 | 🏗️ Build: ${"community".toUpperCase()} (${"prod".toUpperCase()} mode)`);
|
|
78
78
|
}
|
|
79
79
|
description={
|
|
80
80
|
displayName: "ConnectWise PSA",
|
|
@@ -31,6 +31,12 @@ __export(field_collector_exports, {
|
|
|
31
31
|
|
|
32
32
|
module.exports = __toCommonJS(field_collector_exports);
|
|
33
33
|
|
|
34
|
+
function isEmptyFixedCollection(value) {
|
|
35
|
+
if (!value || typeof value != "object" || Array.isArray(value)) return !1;
|
|
36
|
+
const entries = Object.entries(value);
|
|
37
|
+
return entries.length === 0 ? !0 : entries.every(([, v]) => Array.isArray(v) && v.length === 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
function collectFieldData(context, schema, operation, index) {
|
|
35
41
|
const schemaFields = {}, additionalFields = {}, assignments = {}, relevantProperties = [], isPropertyShown = property => {
|
|
36
42
|
if (!property.displayOptions?.show?.operation) return !0;
|
|
@@ -40,7 +46,10 @@ function collectFieldData(context, schema, operation, index) {
|
|
|
40
46
|
const fullName = prefix + property.name;
|
|
41
47
|
try {
|
|
42
48
|
const value = context.getNodeParameter(fullName, index, void 0);
|
|
43
|
-
if (value != null && value !== "")
|
|
49
|
+
if (value != null && value !== "") {
|
|
50
|
+
if (property.type === "fixedCollection" && isEmptyFixedCollection(value)) return;
|
|
51
|
+
if (property.type === "collection" && typeof value == "object") for (const [collectionField, collectionValue] of Object.entries(value)) collectionValue != null && collectionValue !== "" && (target[`${fullName}.${collectionField}`] = collectionValue); else target[fullName] = value;
|
|
52
|
+
}
|
|
44
53
|
} catch {}
|
|
45
54
|
}, UI_CONTROL_FIELDS = [ "operation", "assignments", "endpoint", "ticketType", "parentId", "id" ];
|
|
46
55
|
for (const field of schema.fields) if (!UI_CONTROL_FIELDS.includes(field.name) && field.x_noSubmit !== !0 && isPropertyShown(field)) if (relevantProperties.push(field),
|
|
@@ -33,9 +33,416 @@ module.exports = __toCommonJS(alert_consolidation_exports);
|
|
|
33
33
|
|
|
34
34
|
var import_n8n_workflow = require("n8n-workflow"), import_luxon = require("luxon"), import_crypto = require("crypto"), import_client = require("../../transport/client"), import_utils = require("../../helpers/utils");
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const FINGERPRINT = `\n\x3c!--msp-copilot--\x3e`, FINGERPRINT_FAILURE = `\n\x3c!--msp-copilot:failure--\x3e`, FINGERPRINT_SUCCESS = `\n\x3c!--msp-copilot:success--\x3e`;
|
|
37
|
+
|
|
38
|
+
function buildStatus(value) {
|
|
39
|
+
return typeof value == "number" ? {
|
|
40
|
+
id: value
|
|
41
|
+
} : {
|
|
42
|
+
name: value
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function resolvePriority(context, value) {
|
|
47
|
+
if (value == null || value === "") return null;
|
|
48
|
+
if (typeof value == "number") return {
|
|
49
|
+
id: value
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const priorities = await import_client.connectWiseApiRequest.call(context, "GET", "/service/priorities", {}, {
|
|
53
|
+
pageSize: 1e3,
|
|
54
|
+
fields: "id,name",
|
|
55
|
+
optionsCache: !0
|
|
56
|
+
}), normalizedName = value.trim().toLowerCase(), priority = priorities.find(p => p.name.toLowerCase() === normalizedName);
|
|
57
|
+
return priority ? {
|
|
58
|
+
id: priority.id
|
|
59
|
+
} : (console.warn(`⚠️ Priority "${value}" not found in API response`), null);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return console.error("Error fetching priorities:", error), null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function trimSummary(summary) {
|
|
66
|
+
if (summary.length <= 100) return summary;
|
|
67
|
+
let trimmed = summary.substring(0, 97).trim();
|
|
68
|
+
return trimmed.endsWith(".") ? trimmed : trimmed + "...";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatTimestamp(date = /* @__PURE__ */ new Date) {
|
|
72
|
+
return `\`${import_luxon.DateTime.fromJSDate(date).toFormat("yyyy-MM-dd ccc hh:mm:ss a")}\``;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatMacForSearch(mac) {
|
|
76
|
+
if (!mac) return "";
|
|
77
|
+
if (mac.includes("*")) return mac.replace(/[:\-\.\s]/g, "");
|
|
78
|
+
const cleanMac = mac.replace(/[:\-\.\s]/g, "").toUpperCase();
|
|
79
|
+
return cleanMac.length < 6 ? cleanMac : (cleanMac.match(/.{1,2}/g) || []).join("%");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function findConfiguration(companyId, configurationTypeId, macAddress, ipAddress, hostnameFull, hostnameSearch) {
|
|
83
|
+
try {
|
|
84
|
+
const orConditions = [];
|
|
85
|
+
if (macAddress) {
|
|
86
|
+
const macSearchPattern = formatMacForSearch(macAddress);
|
|
87
|
+
orConditions.push(`macAddress like "%${macSearchPattern}%"`);
|
|
88
|
+
}
|
|
89
|
+
if (ipAddress && orConditions.push(`ipAddress="${ipAddress}"`), hostnameFull) if (hostnameFull.includes("*")) {
|
|
90
|
+
const hostnamePattern = hostnameFull.replace(/\*/g, "%");
|
|
91
|
+
orConditions.push(`name like "${hostnamePattern}"`);
|
|
92
|
+
} else orConditions.push(`name="${hostnameFull}"`);
|
|
93
|
+
if (hostnameSearch && orConditions.push(`name="${hostnameSearch}"`, `name like "${hostnameSearch}.%"`),
|
|
94
|
+
orConditions.length === 0) return console.log("⚠️ No configuration search criteria provided"),
|
|
95
|
+
null;
|
|
96
|
+
const fullCondition = [ `company/id=${companyId}`, "activeFlag=true", configurationTypeId ? Array.isArray(configurationTypeId) ? configurationTypeId.length > 0 ? `type/id in (${configurationTypeId.join(",")})` : null : `type/id=${configurationTypeId}` : null, `(${orConditions.join(" or ")})` ].filter(Boolean).join(" and ");
|
|
97
|
+
console.log(`🔍 Searching configurations with condition: ${fullCondition}`);
|
|
98
|
+
const configs = await import_client.connectWiseApiRequest.call(this, "GET", "/company/configurations", {}, {
|
|
99
|
+
conditions: fullCondition,
|
|
100
|
+
pageSize: 100,
|
|
101
|
+
orderBy: "id desc",
|
|
102
|
+
fields: "id,name,ipAddress,macAddress,serialNumber,modelNumber,tagNumber,notes"
|
|
103
|
+
});
|
|
104
|
+
if (!configs || configs.length === 0) return console.log("⚠️ No configuration found matching the provided identifiers"),
|
|
105
|
+
null;
|
|
106
|
+
if (console.log(`📊 Found ${configs.length} matching configuration(s), applying priority filter...`),
|
|
107
|
+
macAddress) {
|
|
108
|
+
const macPattern = formatMacForSearch(macAddress).toLowerCase();
|
|
109
|
+
for (const config of configs) if (config.macAddress) {
|
|
110
|
+
const configMac = config.macAddress.replace(/[:\-\.\s]/g, "").toLowerCase();
|
|
111
|
+
if (new RegExp(macPattern.replace(/\*/g, ".*"), "i").test(configMac)) return console.log(`✅ Configuration found by MAC address: ${config.name} (MAC: ${config.macAddress})`),
|
|
112
|
+
config;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (ipAddress) {
|
|
116
|
+
if (ipAddress === "127.0.0.1") console.log("❗ Ignoring IP address 127.0.0.1 (localhost)"); else for (const config of configs) if (config.ipAddress === ipAddress) return console.log(`✅ Configuration found by IP address: ${config.name} (IP: ${config.ipAddress})`),
|
|
117
|
+
config;
|
|
118
|
+
}
|
|
119
|
+
if (hostnameFull && !hostnameFull.includes("*")) {
|
|
120
|
+
for (const config of configs) if (config.name && config.name.toLowerCase() === hostnameFull.toLowerCase()) return console.log(`✅ Configuration found by exact hostname: ${config.name}`),
|
|
121
|
+
config;
|
|
122
|
+
}
|
|
123
|
+
if (hostnameFull && hostnameFull.includes("*")) {
|
|
124
|
+
const pattern = new RegExp("^" + hostnameFull.replace(/\*/g, ".*") + "$", "i");
|
|
125
|
+
for (const config of configs) if (config.name && pattern.test(config.name)) return console.log(`✅ Configuration found by hostname pattern: ${config.name}`),
|
|
126
|
+
config;
|
|
127
|
+
}
|
|
128
|
+
if (hostnameSearch) {
|
|
129
|
+
for (const config of configs) if (config.name && config.name.toLowerCase() === hostnameSearch.toLowerCase()) return console.log(`✅ Configuration found by smart hostname (exact): ${config.name}`),
|
|
130
|
+
config;
|
|
131
|
+
for (const config of configs) if (config.name && config.name.toLowerCase().startsWith(hostnameSearch.toLowerCase())) return console.log(`✅ Configuration found by smart hostname (prefix): ${config.name}`),
|
|
132
|
+
config;
|
|
133
|
+
}
|
|
134
|
+
console.log("⚠️ No configurations matched priority filters. Found configurations:");
|
|
135
|
+
for (const config of configs) console.log(` - ${config.name} (ID: ${config.id}, IP: ${config.ipAddress || "N/A"}, MAC: ${config.macAddress || "N/A"})`);
|
|
136
|
+
return null;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return console.error("Error searching for configuration:", error), null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function analyzeTicketNotes(ticketId, maxComments) {
|
|
143
|
+
const fetchLimit = Math.max(10, maxComments), notes = await import_client.connectWiseApiRequest.call(this, "GET", `/service/tickets/${ticketId}/notes`, {}, {
|
|
144
|
+
orderBy: "id desc",
|
|
145
|
+
pageSize: fetchLimit
|
|
146
|
+
}), atMaxComments = notes.length >= maxComments;
|
|
147
|
+
let lastFingerprintedNote = null, lastFingerprintedNoteType = null, hasFailureFingerprint = !1, hasSuccessFingerprint = !1, successCount = 0, failureCount = 0;
|
|
148
|
+
for (const note of notes) {
|
|
149
|
+
const text = note.text || "";
|
|
150
|
+
text.includes("\x3c!--msp-copilot:failure") && (hasFailureFingerprint = !0, failureCount++,
|
|
151
|
+
lastFingerprintedNote || (lastFingerprintedNote = note, lastFingerprintedNoteType = "failure")),
|
|
152
|
+
text.includes("\x3c!--msp-copilot:success") && (hasSuccessFingerprint = !0, successCount++,
|
|
153
|
+
lastFingerprintedNote || (lastFingerprintedNote = note, lastFingerprintedNoteType = "success")),
|
|
154
|
+
!lastFingerprintedNote && text.includes("\x3c!--msp-copilot--\x3e") && (lastFingerprintedNote = note,
|
|
155
|
+
lastFingerprintedNoteType = "failure");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
atMaxComments: atMaxComments,
|
|
159
|
+
lastNote: notes[0] || null,
|
|
160
|
+
lastFingerprintedNote: lastFingerprintedNote,
|
|
161
|
+
lastFingerprintedNoteType: lastFingerprintedNoteType,
|
|
162
|
+
hasFailureFingerprint: hasFailureFingerprint,
|
|
163
|
+
hasSuccessFingerprint: hasSuccessFingerprint,
|
|
164
|
+
successCount: successCount,
|
|
165
|
+
failureCount: failureCount,
|
|
166
|
+
isLastNoteFingerprinted: notes[0] === lastFingerprintedNote
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function searchExistingTicket(searchKey, companyId, boardId, success, options) {
|
|
171
|
+
try {
|
|
172
|
+
const conditions = [ `company/id=${companyId}`, `externalXRef="${searchKey}"` ];
|
|
173
|
+
if (success) {
|
|
174
|
+
if (conditions.push("closedFlag=false", 'slaStatus!="Resolved"'), options.successStatus) {
|
|
175
|
+
const statusObj = buildStatus(options.successStatus);
|
|
176
|
+
"id" in statusObj ? conditions.push(`status/id!=${statusObj.id}`) : conditions.push(`status/name!="${statusObj.name}"`);
|
|
177
|
+
}
|
|
178
|
+
} else if (options.reopenSameBoardOnly && conditions.push(`board/id=${boardId}`),
|
|
179
|
+
options.reopenClosedTicket) {
|
|
180
|
+
const ageConditions = [];
|
|
181
|
+
if (options.reopenTicketMaxAge) {
|
|
182
|
+
const maxAge = (0, import_utils.parseDuration)(options.reopenTicketMaxAge);
|
|
183
|
+
if (maxAge > 0) {
|
|
184
|
+
const cutoffDate = new Date(Date.now() - maxAge).toISOString();
|
|
185
|
+
ageConditions.push(`_info/dateEntered>=[${cutoffDate}]`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (options.reopenTicketLastUpdateAge) {
|
|
189
|
+
const maxUpdateAge = (0, import_utils.parseDuration)(options.reopenTicketLastUpdateAge);
|
|
190
|
+
if (maxUpdateAge > 0) {
|
|
191
|
+
const cutoffDate = new Date(Date.now() - maxUpdateAge).toISOString();
|
|
192
|
+
ageConditions.push(`_info/lastUpdated>=[${cutoffDate}]`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
ageConditions.length > 0 && conditions.push(`(closedFlag=false or (closedFlag=true and ${ageConditions.join(" and ")}))`);
|
|
196
|
+
} else conditions.push("closedFlag=false");
|
|
197
|
+
const tickets = await import_client.connectWiseApiRequest.call(this, "GET", "/service/tickets", {}, {
|
|
198
|
+
conditions: conditions.join(" and "),
|
|
199
|
+
orderBy: "id desc",
|
|
200
|
+
pageSize: 1
|
|
201
|
+
});
|
|
202
|
+
return tickets && tickets.length > 0 ? tickets[0] : null;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return console.error("Error searching for ticket:", error), null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function createTicket(data) {
|
|
209
|
+
console.log(`📝 Creating new ticket with summary: ${data.summary}`);
|
|
210
|
+
const response = await import_client.connectWiseApiRequest.call(this, "POST", "/service/tickets", data);
|
|
211
|
+
return console.log(`✅ Created ticket #${response.id}`), response;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function updateTicketStatus(ticketId, statusValue) {
|
|
215
|
+
const statusObj = buildStatus(statusValue), path = "id" in statusObj ? "status/id" : "status/name", value = "id" in statusObj ? statusObj.id : statusObj.name;
|
|
216
|
+
try {
|
|
217
|
+
return await import_client.connectWiseApiRequest.call(this, "PATCH", `/service/tickets/${ticketId}`, [ {
|
|
218
|
+
op: "replace",
|
|
219
|
+
path: path,
|
|
220
|
+
value: value
|
|
221
|
+
} ]);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.message && error.message.includes("ticket object is invalid") && /Status .* not found/.test(error.message)) return console.log(`⚠️ Status ${value} not valid for ticket #${ticketId}'s current board, skipping status update`),
|
|
224
|
+
!1;
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function createTicketNote(ticketId, text, internalNote) {
|
|
230
|
+
console.log(`📝 Creating note on ticket #${ticketId}`), await import_client.connectWiseApiRequest.call(this, "POST", `/service/tickets/${ticketId}/notes`, {
|
|
231
|
+
text: text,
|
|
232
|
+
internalAnalysisFlag: internalNote,
|
|
233
|
+
detailDescriptionFlag: !internalNote,
|
|
234
|
+
processNotifications: !1
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function updateTicketNote(ticketId, noteId, text) {
|
|
239
|
+
console.log(`📝 Updating note #${noteId} on ticket #${ticketId}`), await import_client.connectWiseApiRequest.call(this, "PATCH", `/service/tickets/${ticketId}/notes/${noteId}`, [ {
|
|
240
|
+
op: "replace",
|
|
241
|
+
path: "text",
|
|
242
|
+
value: text
|
|
243
|
+
} ]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function processSmartTicket(params) {
|
|
247
|
+
const {companyId: companyId, alertName: alertName, alertId: alertId, alertSuccess: alertSuccess, boardId: boardId, failStatus: failStatus, successStatus: successStatus, reopenStatus: reopenStatus, priority: priority, summary: summary, failMessageDetailed: failMessageDetailed, successMessageDetailed: successMessageDetailed, failMessageShort: failMessageShort, successMessageShort: successMessageShort, internalNote: internalNote = !1, reopenClosedTicket: reopenClosedTicket = !1, reopenSameBoardOnly: reopenSameBoardOnly = !0, reopenTicketMaxAge: reopenTicketMaxAge, reopenTicketLastUpdateAge: reopenTicketLastUpdateAge, maxComments: maxComments = 20, appendPreviousNote: appendPreviousNote = !1, appendPreviousNoteTimeframe: appendPreviousNoteTimeframe, appendPreviousNoteOnlyIfLastNote: appendPreviousNoteOnlyIfLastNote = !0, prependToNote: prependToNote = !0, configurationTypeId: configurationTypeId, macAddress: macAddress, ipAddress: ipAddress, hostnameFull: hostnameFull, hostnameSearch: hostnameSearch, skipIfNoConfiguration: skipIfNoConfiguration = !1, appendConfigurationDetails: appendConfigurationDetails = !1, previewMode: previewMode = !1} = params, messageShort = alertSuccess ? successMessageShort : failMessageShort, messageDetailed = alertSuccess ? successMessageDetailed : failMessageDetailed, initialMessage = messageDetailed || messageShort || (alertSuccess ? "Alert success" : "Alert failure"), searchKey = `${alertName}|${alertId}`;
|
|
248
|
+
let noteMessageType, notePhase, fingerprintType = "";
|
|
249
|
+
const existingTicket = await searchExistingTicket.call(this, searchKey, companyId, boardId, alertSuccess, {
|
|
250
|
+
reopenClosedTicket: reopenClosedTicket,
|
|
251
|
+
reopenSameBoardOnly: reopenSameBoardOnly,
|
|
252
|
+
reopenTicketMaxAge: reopenTicketMaxAge,
|
|
253
|
+
reopenTicketLastUpdateAge: reopenTicketLastUpdateAge,
|
|
254
|
+
successStatus: successStatus,
|
|
255
|
+
failStatus: failStatus
|
|
256
|
+
});
|
|
257
|
+
let action, ticket = null, ticketId = null, noteAction, notePreview, configuration = null;
|
|
258
|
+
if (!existingTicket && alertSuccess) return action = "NO_TICKET_TO_RESOLVE", {
|
|
259
|
+
action: action,
|
|
260
|
+
ticket: null,
|
|
261
|
+
ticketId: null,
|
|
262
|
+
previewMode: previewMode,
|
|
263
|
+
noteAction: void 0,
|
|
264
|
+
notePreview: void 0,
|
|
265
|
+
noteMessageType: noteMessageType,
|
|
266
|
+
notePhase: notePhase
|
|
267
|
+
};
|
|
268
|
+
if (!existingTicket && !alertSuccess) {
|
|
269
|
+
if (action = "CREATE_TICKET", (macAddress || ipAddress || hostnameFull || hostnameSearch) && (configuration = await findConfiguration.call(this, companyId, configurationTypeId, macAddress, ipAddress, hostnameFull, hostnameSearch),
|
|
270
|
+
skipIfNoConfiguration && !configuration)) return action = "SKIP_NO_CONFIGURATION",
|
|
271
|
+
{
|
|
272
|
+
action: action,
|
|
273
|
+
ticket: null,
|
|
274
|
+
ticketId: null,
|
|
275
|
+
previewMode: previewMode,
|
|
276
|
+
reason: "No matching configuration found and skipIfNoConfiguration is enabled",
|
|
277
|
+
noteAction: void 0,
|
|
278
|
+
notePreview: void 0,
|
|
279
|
+
noteMessageType: noteMessageType,
|
|
280
|
+
notePhase: notePhase
|
|
281
|
+
};
|
|
282
|
+
let ticketDescription = initialMessage;
|
|
283
|
+
if (appendPreviousNote && !alertSuccess && (ticketDescription += FINGERPRINT_FAILURE),
|
|
284
|
+
appendConfigurationDetails && configuration) {
|
|
285
|
+
let configDetails = "";
|
|
286
|
+
const hardwareDetails = [];
|
|
287
|
+
configuration.serialNumber && hardwareDetails.push(`*Serial Number:* ${configuration.serialNumber}`),
|
|
288
|
+
configuration.modelNumber && hardwareDetails.push(`*Model Number:* ${configuration.modelNumber}`),
|
|
289
|
+
configuration.tagNumber && hardwareDetails.push(`*Tag:* ${configuration.tagNumber}`),
|
|
290
|
+
hardwareDetails.length > 0 && (configDetails += `\n` + hardwareDetails.join(`\n`)),
|
|
291
|
+
configuration.notes && (configDetails += `\nNotes: ${configuration.notes}`), ticketDescription += configDetails;
|
|
292
|
+
}
|
|
293
|
+
const resolvedPriority = await resolvePriority(this, priority), newTicketData = {
|
|
294
|
+
company: {
|
|
295
|
+
id: companyId
|
|
296
|
+
},
|
|
297
|
+
board: {
|
|
298
|
+
id: boardId
|
|
299
|
+
},
|
|
300
|
+
summary: summary,
|
|
301
|
+
status: buildStatus(failStatus),
|
|
302
|
+
initialDescription: ticketDescription,
|
|
303
|
+
externalXRef: searchKey
|
|
304
|
+
};
|
|
305
|
+
if (resolvedPriority ? newTicketData.priority = resolvedPriority : priority && console.warn(`⚠️ Priority "${priority}" could not be resolved, ticket will be created without priority`),
|
|
306
|
+
previewMode) ticket = {
|
|
307
|
+
id: null,
|
|
308
|
+
...newTicketData
|
|
309
|
+
}, ticketId = ticket.id; else if (ticket = await createTicket.call(this, newTicketData),
|
|
310
|
+
ticketId = ticket.id, configuration && ticketId) try {
|
|
311
|
+
await import_client.connectWiseApiRequest.call(this, "POST", `/service/tickets/${ticketId}/configurations`, {
|
|
312
|
+
id: configuration.id
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error(`Warning: Could not attach configuration to ticket #${ticketId}:`, error.message);
|
|
316
|
+
}
|
|
317
|
+
} else if (existingTicket) {
|
|
318
|
+
if (ticket = existingTicket, ticketId = ticket.id, existingTicket.closedFlag && !alertSuccess && reopenClosedTicket) {
|
|
319
|
+
if (action = "REOPEN_TICKET", !previewMode && ticketId) {
|
|
320
|
+
const statusForReopen = reopenStatus && reopenStatus !== "" ? reopenStatus : failStatus, updatedTicket = await updateTicketStatus.call(this, ticketId, statusForReopen);
|
|
321
|
+
updatedTicket !== !1 ? ticket = updatedTicket : action = "REOPEN_TICKET_STATUS_ERROR";
|
|
322
|
+
}
|
|
323
|
+
} else if (alertSuccess && !existingTicket.closedFlag) {
|
|
324
|
+
if (action = "CLOSE_TICKET", !previewMode && ticketId) {
|
|
325
|
+
const updatedTicket = await updateTicketStatus.call(this, ticketId, successStatus);
|
|
326
|
+
updatedTicket !== !1 ? ticket = updatedTicket : action = "CLOSE_TICKET_STATUS_ERROR";
|
|
327
|
+
}
|
|
328
|
+
} else if (!alertSuccess && !existingTicket.closedFlag) {
|
|
329
|
+
if (action = "UPDATE_TICKET_STATUS", !previewMode && ticketId) {
|
|
330
|
+
const statusForUpdate = reopenStatus && reopenStatus !== "" ? reopenStatus : failStatus, updatedTicket = await updateTicketStatus.call(this, ticketId, statusForUpdate);
|
|
331
|
+
updatedTicket !== !1 ? ticket = updatedTicket : action = "UPDATE_TICKET_STATUS_ERROR";
|
|
332
|
+
}
|
|
333
|
+
} else action = "NO_STATUS_UPDATE";
|
|
334
|
+
if (ticketId) {
|
|
335
|
+
const noteAnalysis = await analyzeTicketNotes.call(this, ticketId, maxComments), isPostFirstSuccess = noteAnalysis.hasSuccessFingerprint;
|
|
336
|
+
let shouldAppendToPrevious = !1;
|
|
337
|
+
if (appendPreviousNote && noteAnalysis.lastFingerprintedNote) {
|
|
338
|
+
let canAppend = !0;
|
|
339
|
+
if (canAppend && appendPreviousNoteOnlyIfLastNote && !noteAnalysis.isLastNoteFingerprinted && (canAppend = !1),
|
|
340
|
+
canAppend && appendPreviousNoteTimeframe) {
|
|
341
|
+
const timeframeMs = (0, import_utils.parseDuration)(appendPreviousNoteTimeframe);
|
|
342
|
+
if (timeframeMs > 0) {
|
|
343
|
+
const noteDate = new Date(noteAnalysis.lastFingerprintedNote._info?.lastUpdated || noteAnalysis.lastFingerprintedNote.dateCreated);
|
|
344
|
+
Date.now() - noteDate.getTime() > timeframeMs && (console.log(`📅 Note is too old to append to (note date: ${noteDate.toISOString()}, timeframe: ${appendPreviousNoteTimeframe})`),
|
|
345
|
+
canAppend = !1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
shouldAppendToPrevious = canAppend;
|
|
349
|
+
}
|
|
350
|
+
let noteContent, addFingerprint = !1;
|
|
351
|
+
if (noteMessageType = "fallback", notePhase = "consolidation", !isPostFirstSuccess && !alertSuccess ? (noteContent = messageShort || messageDetailed || "Alert failure",
|
|
352
|
+
noteMessageType = messageShort ? "short" : messageDetailed ? "detailed" : "fallback",
|
|
353
|
+
notePhase = "pre-first-success", shouldAppendToPrevious || (addFingerprint = !0,
|
|
354
|
+
fingerprintType = FINGERPRINT_FAILURE)) : alertSuccess && noteAnalysis.successCount === 0 ? (noteContent = messageDetailed || messageShort || "Alert success",
|
|
355
|
+
noteMessageType = messageDetailed ? "detailed" : messageShort ? "short" : "fallback",
|
|
356
|
+
notePhase = "first-success", shouldAppendToPrevious = !1, addFingerprint = !0, fingerprintType = FINGERPRINT_SUCCESS) : !alertSuccess && noteAnalysis.lastFingerprintedNoteType === "success" ? (noteContent = messageShort || messageDetailed || "Alert failure",
|
|
357
|
+
noteMessageType = messageShort ? "short" : messageDetailed ? "detailed" : "fallback",
|
|
358
|
+
notePhase = "post-first-success", shouldAppendToPrevious = !1, addFingerprint = !0,
|
|
359
|
+
fingerprintType = FINGERPRINT_FAILURE) : (noteContent = messageShort || messageDetailed || (alertSuccess ? "Alert success" : "Alert failure"),
|
|
360
|
+
noteMessageType = messageShort ? "short" : messageDetailed ? "detailed" : "fallback",
|
|
361
|
+
notePhase = "consolidation", shouldAppendToPrevious || (addFingerprint = !0, fingerprintType = alertSuccess ? FINGERPRINT_SUCCESS : FINGERPRINT_FAILURE)),
|
|
362
|
+
shouldAppendToPrevious && noteAnalysis.lastFingerprintedNote) {
|
|
363
|
+
noteAction = "APPEND_TO_PREVIOUS_NOTE";
|
|
364
|
+
const cleanedContent = noteAnalysis.lastFingerprintedNote.text.replace(/\n<!--msp-copilot[^>]*-->/g, ""), existingIsDetailed = !cleanedContent.match(/^`\d{4}-\d{2}-\d{2}/), appendingDetailedMessage = noteMessageType === "detailed", separator = existingIsDetailed || appendingDetailedMessage ? `\n\n` : `\n`, timestampSeparator = appendingDetailedMessage ? `\n` : " ", newEntry = `${formatTimestamp()}${timestampSeparator}${noteContent}`, combinedContent = prependToNote ? `${newEntry}${separator}${cleanedContent}` : `${cleanedContent}${separator}${newEntry}`;
|
|
365
|
+
if (notePreview = newEntry, !previewMode) {
|
|
366
|
+
const originalFingerprint = noteAnalysis.lastFingerprintedNote.text.match(/\n<!--msp-copilot[^>]*-->/)?.[0] || FINGERPRINT;
|
|
367
|
+
await updateTicketNote.call(this, ticketId, noteAnalysis.lastFingerprintedNote.id, combinedContent + originalFingerprint);
|
|
368
|
+
}
|
|
369
|
+
} else if (noteAnalysis.atMaxComments) {
|
|
370
|
+
if (noteAction = "UPDATE_LAST_NOTE", noteAnalysis.lastNote) {
|
|
371
|
+
const cleanedContent = noteAnalysis.lastNote.text.replace(/\n<!--msp-copilot[^>]*-->/g, ""), existingIsDetailed = !cleanedContent.match(/^`\d{4}-\d{2}-\d{2}/), appendingDetailedMessage = noteMessageType === "detailed", separator = existingIsDetailed || appendingDetailedMessage ? `\n\n` : `\n`, timestampSeparator = appendingDetailedMessage ? `\n` : " ", newEntry = `${formatTimestamp()}${timestampSeparator}${noteContent}`, combinedContent = prependToNote ? `${newEntry}${separator}${cleanedContent}` : `${cleanedContent}${separator}${newEntry}`;
|
|
372
|
+
notePreview = newEntry, previewMode || await updateTicketNote.call(this, ticketId, noteAnalysis.lastNote.id, combinedContent + fingerprintType);
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
noteAction = "CREATE_NOTE";
|
|
376
|
+
const isFirstSuccess = notePhase === "first-success", timestampSeparator = !isFirstSuccess && noteMessageType === "detailed" ? `\n` : " ", newEntry = isFirstSuccess ? noteContent : `${formatTimestamp()}${timestampSeparator}${noteContent}`;
|
|
377
|
+
if (notePreview = newEntry, !previewMode) {
|
|
378
|
+
const finalNoteText = addFingerprint ? newEntry + fingerprintType : newEntry;
|
|
379
|
+
await createTicketNote.call(this, ticketId, finalNoteText, internalNote);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
action: action,
|
|
386
|
+
noteAction: noteAction,
|
|
387
|
+
notePreview: notePreview,
|
|
388
|
+
noteMessageType: noteMessageType,
|
|
389
|
+
notePhase: notePhase,
|
|
390
|
+
ticketId: ticketId,
|
|
391
|
+
previewMode: previewMode,
|
|
392
|
+
ticket: ticket,
|
|
393
|
+
configuration: configuration || void 0
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const executeSmartConsolidationPro = async function(itemIndex) {
|
|
398
|
+
try {
|
|
399
|
+
const companyId = this.getNodeParameter("company", itemIndex), alertName = this.getNodeParameter("alertName", itemIndex).trim().substring(0, 40), alertIdRaw = this.getNodeParameter("alertId", itemIndex).trim(), alertId = alertIdRaw.length <= 50 ? alertIdRaw : (0,
|
|
400
|
+
import_crypto.createHash)("md5").update(alertIdRaw).digest("hex"), alertSuccess = this.getNodeParameter("alertSuccess", itemIndex), boardId = this.getNodeParameter("board", itemIndex), failStatus = this.getNodeParameter("failStatus", itemIndex), successStatus = this.getNodeParameter("successStatus", itemIndex), priority = this.getNodeParameter("priority", itemIndex), summary = trimSummary(this.getNodeParameter("summary", itemIndex)), failMessageDetailed = this.getNodeParameter("failMessageDetailed", itemIndex, ""), successMessageDetailed = this.getNodeParameter("successMessageDetailed", itemIndex, ""), failMessageShort = this.getNodeParameter("failMessageShort", itemIndex, ""), successMessageShort = this.getNodeParameter("successMessageShort", itemIndex, ""), internalNote = this.getNodeParameter("internalNote", itemIndex, !1), reopenClosedTicket = this.getNodeParameter("reopenClosedTicket", itemIndex, !1), reopenSameBoardOnly = this.getNodeParameter("reopenSameBoardOnly", itemIndex, !0), reopenStatus = this.getNodeParameter("reopenStatus", itemIndex, ""), reopenTicketMaxAge = this.getNodeParameter("reopenTicketMaxAge", itemIndex, ""), reopenTicketLastUpdateAge = this.getNodeParameter("reopenTicketLastUpdateAge", itemIndex, ""), maxComments = this.getNodeParameter("maxComments", itemIndex, 20), appendPreviousNote = this.getNodeParameter("appendPreviousNote", itemIndex, !1), appendPreviousNoteTimeframe = this.getNodeParameter("appendPreviousNoteTimeframe", itemIndex, ""), appendPreviousNoteOnlyIfLastNote = this.getNodeParameter("appendPreviousNoteOnlyIfLastNote", itemIndex, !0), prependToNote = this.getNodeParameter("prependToNote", itemIndex, !0), configAttachment = this.getNodeParameter("configAttachment", itemIndex, {}), configurationTypeId = configAttachment.configurationTypeId, macAddress = configAttachment.macAddress, ipAddress = configAttachment.ipAddress, hostnameFull = configAttachment.hostnameFull, hostnameSearch = configAttachment.hostnameSearch, skipIfNoConfiguration = configAttachment.skipIfNoConfiguration, appendConfigurationDetails = configAttachment.appendConfigurationDetails, previewMode = this.getNodeParameter("previewMode", itemIndex, !1), params = {
|
|
401
|
+
companyId: companyId,
|
|
402
|
+
alertName: alertName,
|
|
403
|
+
alertId: alertId,
|
|
404
|
+
alertSuccess: alertSuccess,
|
|
405
|
+
boardId: boardId,
|
|
406
|
+
failStatus: failStatus,
|
|
407
|
+
successStatus: successStatus,
|
|
408
|
+
reopenStatus: reopenStatus,
|
|
409
|
+
priority: priority,
|
|
410
|
+
summary: summary,
|
|
411
|
+
failMessageDetailed: failMessageDetailed,
|
|
412
|
+
successMessageDetailed: successMessageDetailed,
|
|
413
|
+
failMessageShort: failMessageShort,
|
|
414
|
+
successMessageShort: successMessageShort,
|
|
415
|
+
internalNote: internalNote,
|
|
416
|
+
reopenClosedTicket: reopenClosedTicket,
|
|
417
|
+
reopenSameBoardOnly: reopenSameBoardOnly,
|
|
418
|
+
reopenTicketMaxAge: reopenTicketMaxAge,
|
|
419
|
+
reopenTicketLastUpdateAge: reopenTicketLastUpdateAge,
|
|
420
|
+
maxComments: maxComments,
|
|
421
|
+
appendPreviousNote: appendPreviousNote,
|
|
422
|
+
appendPreviousNoteTimeframe: appendPreviousNoteTimeframe,
|
|
423
|
+
appendPreviousNoteOnlyIfLastNote: appendPreviousNoteOnlyIfLastNote,
|
|
424
|
+
prependToNote: prependToNote,
|
|
425
|
+
configurationTypeId: configurationTypeId,
|
|
426
|
+
macAddress: macAddress,
|
|
427
|
+
ipAddress: ipAddress,
|
|
428
|
+
hostnameFull: hostnameFull,
|
|
429
|
+
hostnameSearch: hostnameSearch,
|
|
430
|
+
skipIfNoConfiguration: skipIfNoConfiguration,
|
|
431
|
+
appendConfigurationDetails: appendConfigurationDetails,
|
|
432
|
+
previewMode: previewMode
|
|
433
|
+
};
|
|
434
|
+
return [ {
|
|
435
|
+
json: await processSmartTicket.call(this, params),
|
|
436
|
+
pairedItem: {
|
|
437
|
+
item: itemIndex
|
|
438
|
+
}
|
|
439
|
+
} ];
|
|
440
|
+
} catch (error) {
|
|
441
|
+
throw new import_n8n_workflow.NodeOperationError(this.getNode(), `Smart Alert Consolidation failed: ${error.message}`, {
|
|
442
|
+
description: error.description
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}, executeSmartConsolidation = executeSmartConsolidationPro;
|
|
39
446
|
|
|
40
447
|
0 && (module.exports = {
|
|
41
448
|
executeSmartConsolidation: executeSmartConsolidation
|
|
@@ -75,6 +75,11 @@ const ticketFields = [ {
|
|
|
75
75
|
value: "update",
|
|
76
76
|
description: "Update a ticket",
|
|
77
77
|
action: "Update a ticket"
|
|
78
|
+
}, {
|
|
79
|
+
name: "Smart Alert Consolidation ✨",
|
|
80
|
+
value: "smartAlertConsolidation",
|
|
81
|
+
description: "Create, update or close tickets with smart alert consolidation",
|
|
82
|
+
action: "Smart alert consolidation ✨"
|
|
78
83
|
} ],
|
|
79
84
|
default: "create"
|
|
80
85
|
}, {
|
|
@@ -995,11 +1000,6 @@ const ticketFields = [ {
|
|
|
995
1000
|
|
|
996
1001
|
async function execute(operation, i) {
|
|
997
1002
|
if (operation === "smartAlertConsolidation") {
|
|
998
|
-
{
|
|
999
|
-
if (process.env.MSPCOPILOT_BETA !== "true") return (0, import_utils.returnProOnlyMessage)(this, i);
|
|
1000
|
-
const {requireAccessKey: requireAccessKey} = await import("../../transport/access-key.js");
|
|
1001
|
-
if (!await requireAccessKey(this, "beta")) return (0, import_utils.returnAccessKeyRequired)(this, i);
|
|
1002
|
-
}
|
|
1003
1003
|
const {executeSmartConsolidation: executeSmartConsolidation} = await import("./alert-consolidation.js");
|
|
1004
1004
|
return executeSmartConsolidation.call(this, i);
|
|
1005
1005
|
}
|
|
@@ -353,6 +353,13 @@ const timeEntryFields = [ {
|
|
|
353
353
|
default: !1,
|
|
354
354
|
description: "Whether to email the resource about this time entry",
|
|
355
355
|
x_submitAs: "emailResourceFlag"
|
|
356
|
+
}, {
|
|
357
|
+
displayName: "Email CC",
|
|
358
|
+
name: "emailCcFlag",
|
|
359
|
+
type: "boolean",
|
|
360
|
+
default: !1,
|
|
361
|
+
description: "Whether to email the CC list on the ticket about this time entry",
|
|
362
|
+
x_submitAs: "emailCcFlag"
|
|
356
363
|
}, {
|
|
357
364
|
displayName: "Email Contact",
|
|
358
365
|
name: "emailContactFlag",
|
|
@@ -39,7 +39,7 @@ module.exports = __toCommonJS(version_check_exports);
|
|
|
39
39
|
|
|
40
40
|
var fs = __toESM(require("fs/promises")), path = __toESM(require("path")), os = __toESM(require("os")), import_common = require("../descriptions/common.descriptions"), import_hint_collector = require("../helpers/hint-collector");
|
|
41
41
|
|
|
42
|
-
const CACHE_FILE_PATH = path.join(os.tmpdir(), "connectwise-n8n-version-cache.json"), CURRENT_VERSION = "0.2.
|
|
42
|
+
const CACHE_FILE_PATH = path.join(os.tmpdir(), "connectwise-n8n-version-cache.json"), CURRENT_VERSION = "0.2.4";
|
|
43
43
|
|
|
44
44
|
let inMemoryCache = null, cacheLoadPromise = null, fetchInProgress = !1;
|
|
45
45
|
|