201 lines
6.3 KiB
JavaScript
201 lines
6.3 KiB
JavaScript
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
|
|
}
|