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