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

161 lines
5.8 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; // "@filename" или "@@filename"
prefix: string; // "@" или "@@"
name: string; // "filename"
startIndex: number;
endIndex: number;
type: 'file' | 'prompt';
}
export interface FileReference {
type: 'file' | 'prompt';
name: string;
path: string;
permanent: boolean; // true для @@ и ##
// TODO: добавить snapshotId при реализации снапшотов
}
// ----------------------------------------------- Reference Parser ---------------------------------------------------------------
export class ReferenceParser {
/**
* Парсит текст и находит все @/@@ и #/## ссылки
* @param text - текст для парсинга
* @returns массив найденных ссылок
*/
static parseReferences(text: string): ReferenceMatch[] {
const matches: ReferenceMatch[] = [];
// Регулярное выражение для поиска @/@@ и #/## ссылок
// Поддерживает имена файлов с пробелами, заключенные в кавычки или без пробелов
const regex = /(@{1,2}|#{1,2})(?:"([^"]+)"|([^\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;
matches.push({
fullMatch: match[0],
prefix: prefix,
name: name,
startIndex: match.index,
endIndex: match.index + match[0].length,
type: prefix.startsWith('@') ? 'file' : 'prompt'
});
}
return matches;
}
/**
* Проверяет, является ли позиция курсора внутри незавершённой ссылки
* @param text - текст сообщения
* @param cursorPosition - позиция курсора
* @returns информация о текущей ссылке или null
*/
static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null {
// Ищем незавершенные ссылки перед курсором
const beforeCursor = text.substring(0, cursorPosition);
// Регулярное выражение для поиска начала ссылки без завершения
const incompleteRegex = /(@{1,2}|#{1,2})([^\s@#"]*?)$/;
const match = beforeCursor.match(incompleteRegex);
if (match) {
const prefix = match[1];
const partialName = match[2];
const startIndex = beforeCursor.length - match[0].length;
return {
fullMatch: match[0],
prefix: prefix,
name: partialName,
startIndex: startIndex,
endIndex: cursorPosition,
type: prefix.startsWith('@') ? 'file' : 'prompt'
};
}
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;
}
}