Introduces integration-based webhook registration, message parsing, dry-run monitoring, JSM ticket creation, and OAuth token refresh for DC Ops spaces. Co-authored-by: Cursor <cursoragent@cursor.com>
145 lines
4 KiB
JavaScript
145 lines
4 KiB
JavaScript
function escapeRegex(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function buildKeywordPattern(keywords) {
|
|
const sorted = [...keywords].sort((a, b) => b.length - a.length);
|
|
const parts = sorted.map(keyword => escapeRegex(keyword).replace(/\s+/g, '\\s+'));
|
|
return new RegExp(`(?:${parts.join('|')})`, 'i');
|
|
}
|
|
|
|
function findIntentMatch(textLower, intents) {
|
|
let bestMatch = null;
|
|
|
|
for (const [intentKey, intentConfig] of Object.entries(intents || {})) {
|
|
const keywords = intentConfig?.keywords || [];
|
|
if (keywords.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
const pattern = buildKeywordPattern(keywords);
|
|
const match = textLower.match(pattern);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
|
|
const matchIndex = match.index ?? -1;
|
|
if (!bestMatch || matchIndex < bestMatch.matchIndex) {
|
|
bestMatch = {
|
|
intentKey,
|
|
intentConfig,
|
|
matchIndex,
|
|
matchedKeyword: match[0],
|
|
};
|
|
}
|
|
}
|
|
|
|
return bestMatch;
|
|
}
|
|
|
|
function extractJobNames(text, jobNamePattern) {
|
|
const pattern = new RegExp(jobNamePattern, 'g');
|
|
const matches = [];
|
|
let match;
|
|
|
|
while ((match = pattern.exec(text)) !== null) {
|
|
matches.push({
|
|
jobName: match[0],
|
|
index: match.index,
|
|
});
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
function pickJobName(jobMatches, intentMatchIndex) {
|
|
if (jobMatches.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (jobMatches.length === 1) {
|
|
return jobMatches[0].jobName;
|
|
}
|
|
|
|
let closest = jobMatches[0];
|
|
let closestDistance = Math.abs(jobMatches[0].index - intentMatchIndex);
|
|
|
|
for (const candidate of jobMatches.slice(1)) {
|
|
const distance = Math.abs(candidate.index - intentMatchIndex);
|
|
if (distance < closestDistance) {
|
|
closest = candidate;
|
|
closestDistance = distance;
|
|
}
|
|
}
|
|
|
|
return closest.jobName;
|
|
}
|
|
|
|
function applyTemplate(template, values) {
|
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => values[key] ?? '');
|
|
}
|
|
|
|
export function parseJobMessage({ text, roomConfig }) {
|
|
const originalText = (text || '').trim();
|
|
|
|
if (!originalText) {
|
|
return { outcome: 'skip', skipReason: 'empty_message' };
|
|
}
|
|
|
|
const textLower = originalText.toLowerCase();
|
|
const intentMatch = findIntentMatch(textLower, roomConfig?.intents);
|
|
|
|
if (!intentMatch) {
|
|
return { outcome: 'skip', skipReason: 'no_intent', originalText };
|
|
}
|
|
|
|
const jobMatches = extractJobNames(originalText, roomConfig.jobNamePattern);
|
|
const jobName = pickJobName(jobMatches, intentMatch.matchIndex);
|
|
|
|
if (!jobName) {
|
|
return {
|
|
outcome: 'skip',
|
|
skipReason: 'no_job_name',
|
|
intent: intentMatch.intentKey,
|
|
originalText,
|
|
};
|
|
}
|
|
|
|
return {
|
|
outcome: 'match',
|
|
intent: intentMatch.intentKey,
|
|
intentConfig: intentMatch.intentConfig,
|
|
jobName,
|
|
originalText,
|
|
matchedKeyword: intentMatch.matchedKeyword,
|
|
};
|
|
}
|
|
|
|
export function buildTicketFields({ roomConfig, parseResult, personEmail }) {
|
|
const { intent, intentConfig, jobName, originalText } = parseResult;
|
|
const jiraDefaults = roomConfig.jiraDefaults || {};
|
|
|
|
const summary = applyTemplate(intentConfig.summaryTemplate, { jobName, personEmail, originalText });
|
|
const description = applyTemplate(intentConfig.descriptionTemplate, {
|
|
jobName,
|
|
personEmail: personEmail || 'unknown',
|
|
originalText,
|
|
});
|
|
|
|
const requestFieldValues = {
|
|
summary,
|
|
description,
|
|
customfield_10231: [{ value: jiraDefaults.environment || 'PROD' }],
|
|
};
|
|
|
|
if (intentConfig.urgency) {
|
|
requestFieldValues.customfield_10264 = { value: intentConfig.urgency };
|
|
}
|
|
|
|
return {
|
|
serviceDeskId: String(jiraDefaults.serviceDeskId),
|
|
requestTypeId: String(jiraDefaults.requestTypeId),
|
|
requestFieldValues,
|
|
raiseOnBehalfOf: personEmail || undefined,
|
|
};
|
|
}
|