import { getDisplayFileName } from '@/utils/file_utils' export const mentionTypePrefixMap = { file: 'file', knowledge: 'knowledge', mcp: 'mcp', skill: 'skill', subagent: 'subagent' } const mentionTypePattern = Object.values(mentionTypePrefixMap).join('|') const mentionTokenRegex = new RegExp( `@(${mentionTypePattern}):(?:"((?:\\\\.|[^"\\\\])*)"|(\\S+))`, 'g' ) const quoteMentionValue = (value) => String(value ?? '') .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') const unquoteMentionValue = (value) => String(value ?? '').replace(/\\(["\\])/g, '$1') export const formatMentionToken = (type, value) => { const prefix = mentionTypePrefixMap[type] || type const rawValue = String(value ?? '') if (/\s|["\\]/.test(rawValue)) { return `@${prefix}:"${quoteMentionValue(rawValue)}"` } return `@${prefix}:${rawValue}` } export const parseMentionText = (text = '') => { const value = String(text || '') const segments = [] let lastIndex = 0 mentionTokenRegex.lastIndex = 0 for (const match of value.matchAll(mentionTokenRegex)) { const raw = match[0] const start = match.index ?? 0 const end = start + raw.length if (start > lastIndex) { segments.push({ kind: 'text', text: value.slice(lastIndex, start), start: lastIndex, end: start }) } const type = match[1] const quotedValue = match[2] const rawValue = match[3] segments.push({ kind: 'mention', raw, type, value: quotedValue !== undefined ? unquoteMentionValue(quotedValue) : rawValue, start, end }) lastIndex = end } if (lastIndex < value.length) { segments.push({ kind: 'text', text: value.slice(lastIndex), start: lastIndex, end: value.length }) } return segments } const setMentionLabel = (labels, type, value, label) => { const rawValue = String(value ?? '').trim() const displayLabel = String(label || '').trim() if (!rawValue || !displayLabel) return labels[`${type}:${rawValue}`] = displayLabel } export const buildMentionDisplayLabels = (mention = {}) => { const labels = {} ;(mention.knowledgeBases || []).forEach((kb) => { const label = kb?.name || kb?.label || kb?.kb_id || kb?.value || '' setMentionLabel(labels, 'knowledge', kb?.name, label) setMentionLabel(labels, 'knowledge', kb?.label, label) setMentionLabel(labels, 'knowledge', kb?.kb_id, label) setMentionLabel(labels, 'knowledge', kb?.value, label) }) ;(mention.mcps || []).forEach((mcp) => { const label = mcp?.name || mcp?.label || mcp?.slug || mcp?.id || mcp?.value || '' setMentionLabel(labels, 'mcp', mcp?.slug, label) setMentionLabel(labels, 'mcp', mcp?.id, label) setMentionLabel(labels, 'mcp', mcp?.value, label) setMentionLabel(labels, 'mcp', mcp?.name, label) }) ;(mention.skills || []).forEach((skill) => { const label = skill?.name || skill?.label || skill?.slug || skill?.id || skill?.value || '' setMentionLabel(labels, 'skill', skill?.slug, label) setMentionLabel(labels, 'skill', skill?.id, label) setMentionLabel(labels, 'skill', skill?.value, label) setMentionLabel(labels, 'skill', skill?.name, label) }) ;(mention.subagents || []).forEach((subagent) => { const label = subagent?.name || subagent?.label || subagent?.id || subagent?.value || '' setMentionLabel(labels, 'subagent', subagent?.id, label) setMentionLabel(labels, 'subagent', subagent?.value, label) setMentionLabel(labels, 'subagent', subagent?.slug, label) setMentionLabel(labels, 'subagent', subagent?.name, label) }) return labels } export const getMentionDisplayLabel = (type, value, displayLabels = {}) => { const mappedLabel = displayLabels[`${type}:${value}`] if (mappedLabel) return mappedLabel if (type === 'file') { const normalizedPath = String(value ?? '').replace(/\/+$/, '') return getDisplayFileName(normalizedPath || value, '文件') } return String(value ?? '').trim() || type } export const findActiveMentionQuery = (text = '', rawCaretOffset = 0) => { const value = String(text || '') const offset = Math.max(0, Math.min(rawCaretOffset, value.length)) const segments = parseMentionText(value) const mentionAtCursor = segments.find( (segment) => segment.kind === 'mention' && offset > segment.start && offset <= segment.end ) if (mentionAtCursor) return null const textBeforeCursor = value.slice(0, offset) const atIndex = textBeforeCursor.lastIndexOf('@') if (atIndex === -1) return null const query = textBeforeCursor.slice(atIndex + 1) if (query.includes('@') || query.includes(':') || /\s/.test(query)) return null return { start: atIndex, end: offset, query } } export const replaceRawRange = (text = '', start = 0, end = start, replacement = '') => { const value = String(text || '') const safeStart = Math.max(0, Math.min(start, value.length)) const safeEnd = Math.max(safeStart, Math.min(end, value.length)) return value.slice(0, safeStart) + replacement + value.slice(safeEnd) } export const expandMentionDeletionRange = ( text = '', start = 0, end = start, direction = 'backward' ) => { const value = String(text || '') const safeStart = Math.max(0, Math.min(start, value.length)) const safeEnd = Math.max(safeStart, Math.min(end, value.length)) const mentions = parseMentionText(value).filter((segment) => segment.kind === 'mention') if (safeStart !== safeEnd) { const touchedMentions = mentions.filter( (segment) => segment.start < safeEnd && segment.end > safeStart ) if (!touchedMentions.length) return null return { start: Math.min(safeStart, ...touchedMentions.map((segment) => segment.start)), end: Math.max(safeEnd, ...touchedMentions.map((segment) => segment.end)) } } const cursor = safeStart if (direction === 'forward') { const mention = mentions.find((segment) => cursor >= segment.start && cursor < segment.end) if (!mention) return null const includeTrailingSpace = value[mention.end] === ' ' return { start: mention.start, end: includeTrailingSpace ? mention.end + 1 : mention.end } } const mention = mentions.find((segment) => cursor > segment.start && cursor <= segment.end) if (mention) { return { start: mention.start, end: mention.end } } return null }