jiraCloud/services/jobMessageParser.js
jmcqueen efc64a227c Add Webex inbound OAuth flow and room-to-Jira job request pipeline.
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>
2026-07-28 08:27:52 -04:00

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,
};
}