llm-agent-plugin/src/references/ReferenceParser.ts

182 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Парсер для обработки ссылок на файлы (@/@@) и промпты (#/##) в тексте сообщений.
* Поддерживает поиск активных ссылок при вводе и валидацию промптов.
*/
// ----------------------------------------------- Interfaces ---------------------------------------------------------------
export interface ReferenceMatch {
fullMatch: string;
prefix: string; // Теперь может быть @, !@, #, !#, $, !$
name: string;
startIndex: number;
endIndex: number;
type: 'file' | 'prompt' | 'external' | 'url';
permanent: boolean;
}
export interface FileReference {
type: 'file' | 'prompt' | 'external' | 'url';
name: string;
path: string;
permanent: boolean;
uuid?: string;
}
// ----------------------------------------------- Reference Parser ---------------------------------------------------------------
export class ReferenceParser {
/**
* Парсит текст и находит все @/@@ и #/## ссылки
* @param text - текст для парсинга
* @returns массив найденных ссылок
*/
static parseReferences(text: string): ReferenceMatch[] {
const matches: ReferenceMatch[] = [];
const regex = /(![@#$%]|[@#$%])(?:"([^"]+)"|([^\s@#$%"!]+))/g;
let match;
while ((match = regex.exec(text)) !== null) {
const prefix = match[1];
const quotedName = match[2];
const unquotedName = match[3];
const name = quotedName || unquotedName;
// Определяем тип по первому символу префикса (после !)
const typeChar = prefix.replace('!', '');
let type: 'file' | 'prompt' | 'external'| 'url';
if (typeChar === '@') type = 'file';
else if (typeChar === '#') type = 'prompt';
else if (typeChar === '$') type = 'external';
else type = 'url';
matches.push({
fullMatch: match[0],
prefix: prefix,
name: name,
startIndex: match.index,
endIndex: match.index + match[0].length,
type: type,
permanent: prefix.startsWith('!')
});
}
return matches;
}
/**
* Проверяет, является ли позиция курсора внутри незавершённой ссылки
* @param text - текст сообщения
* @param cursorPosition - позиция курсора
* @returns информация о текущей ссылке или null
*/
static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null {
const beforeCursor = text.substring(0, cursorPosition);
// Измененная регулярка: теперь она понимает, если после символа # идет кавычка
// Группа 1: Префикс, Группа 2: Имя в кавычках, Группа 3: Имя без кавычек
const incompleteRegex = /(![@#$%]|[@#$%])(?:"([^"]*)|([^\s@#$%"!]*?))$/;
const match = beforeCursor.match(incompleteRegex);
if (match) {
const prefix = match[1];
const nameInQuotes = match[2]; // текст внутри кавычек
const nameWithoutQuotes = match[3]; // текст до первого пробела
const partialName = nameInQuotes !== undefined ? nameInQuotes : nameWithoutQuotes;
const startIndex = beforeCursor.length - match[0].length;
const typeChar = prefix.replace('!', '');
let type: 'file' | 'prompt' | 'external' | 'url';
if (typeChar === '@') type = 'file';
else if (typeChar === '#') type = 'prompt';
else if (typeChar === '$') type = 'external';
else type = 'url';
return {
fullMatch: match[0],
prefix: prefix,
name: partialName,
startIndex: startIndex,
endIndex: cursorPosition,
type: type,
permanent: prefix.startsWith('!')
};
}
return null;
}
/**
* Валидирует промпт на корректность @{...} ссылок
* @param promptContent - содержимое промпта
* @returns результат валидации
*/
static validatePromptReferences(promptContent: string): { valid: boolean; errors: string[] } {
const errors: string[] = [];
// Ищем шаблонные ссылки @{...}
const templateRegex = /@\{([^}]+)\}/g;
const matches = promptContent.matchAll(templateRegex);
for (const match of matches) {
const referenceName = match[1].trim();
// Проверяем, что имя ссылки не пустое
if (!referenceName) {
errors.push(`Пустая ссылка в позиции ${match.index}`);
continue;
}
// Проверяем, что имя содержит только допустимые символы
if (!/^[a-zA-Z0-9_-]+$/.test(referenceName)) {
errors.push(`Недопустимые символы в ссылке "${referenceName}"`);
}
}
return {
valid: errors.length === 0,
errors: errors
};
}
/**
* Извлекает все шаблонные ссылки из промпта, основываясь на заданных префиксах.
* По умолчанию ищет ссылки с префиксом @{...}.
* @param promptContent - содержимое промпта
* @param prefixes - массив префиксов, по которым нужно искать ссылки (например, ['@'], ['#'], ['@', '#']). По умолчанию ['@'].
* @returns массив имен ссылок
*/
static extractTemplateReferences(promptContent: string, prefixes: Array<'@' | '#'> = ['@']): string[] {
const references: string[] = [];
let prefixRegexPart: string;
if (prefixes.includes('@') && prefixes.includes('#')) {
prefixRegexPart = '[#@]'; // Ищет как @, так и #
} else if (prefixes.includes('@')) {
prefixRegexPart = '@'; // Ищет только @
} else if (prefixes.includes('#')) {
prefixRegexPart = '#'; // Ищет только #
} else {
// Если пустой массив или неподдерживаемые префиксы, не ищем ничего
return [];
}
// Регулярное выражение для поиска ссылок с динамическим префиксом
const templateRegex = new RegExp(`${prefixRegexPart}\\{([^}]+)\\}`, 'g');
const matches = promptContent.matchAll(templateRegex);
for (const match of matches) {
const referenceName = match[1].trim();
if (referenceName && !references.includes(referenceName)) {
references.push(referenceName);
}
}
return references;
}
}