/
psklyarov
/
agent-replacer
Обзор
Документация
Войти
/
psklyarov
/
agent-replacer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/main/java/com/example/agentreplacer/processor/FileProcessorService.java
173 строки
7 KB
pavelsklyarov
fixed path
15 дек 2025, 16:23
15 дек 2025, 16:23
0d2b09f
Код
Авторство
О чём код?
package com.example.agentreplacer.processor; import com.example.agentreplacer.llm.LlmClient; import com.example.agentreplacer.plan.ChangePlan; import com.example.agentreplacer.chat.ChatService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.regex.Pattern; import java.util.stream.Stream; @Service @RequiredArgsConstructor public class FileProcessorService { private final LlmClient llmClient; private final ProcessingProperties processingProperties; private final ChatService chatService; private static final Logger log = LoggerFactory.getLogger(FileProcessorService.class); public int processRepositoryFiles(Path repoDir, ChangePlan plan, String sessionId, String repoSlug, String featureBranch) throws IOException { log.info("Start processing files: repo={} branch={}", repoSlug, featureBranch); final int[] modifiedCount = {0}; try (Stream<Path> stream = Files.walk(repoDir)) { stream .filter(p -> !isUnderGitDir(repoDir, p)) .filter(Files::isRegularFile) .forEach(path -> { try { log.debug("Check file {}", path); if (!precheckFile(path)) return; String relPath = repoDir.relativize(path).toString(); if (shouldProcess(path, relPath, plan)) { String original = Files.readString(path, StandardCharsets.UTF_8); String updated = applyChanges(original, path, plan); if (!original.equals(updated)) { Files.writeString(path, updated, StandardCharsets.UTF_8); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Изменён файл: " + repoSlug + "/" + relPath + " (ветка: " + featureBranch + ")"); } modifiedCount[0]++; log.debug("File modified: {}", relPath); } } else { log.trace("Skip file by rules: {}", path); } } catch (Exception e) { // логируем и продолжаем log.warn("Failed processing file {}: {}", path, e.getMessage()); } }); } log.info("Files processed: repo={} branch={} modified={}", repoSlug, featureBranch, modifiedCount[0]); return modifiedCount[0]; } boolean shouldProcess(Path path, String relativePath, ChangePlan plan) throws IOException { String pathString = relativePath; // матчим по относительному пути внутри репозитория String content = Files.readString(path, StandardCharsets.UTF_8); boolean include = matchAnyRule(pathString, content, plan.getIncludeRules(), true); boolean exclude = matchAnyRule(pathString, content, plan.getExcludeRules(), false); return include && !exclude; } private boolean matchAnyRule(String pathString, String content, List<ChangePlan.FileRule> rules, boolean defaultIfNull) { if (rules == null || rules.isEmpty()) return defaultIfNull; for (ChangePlan.FileRule rule : rules) { if (ruleMatches(pathString, content, rule)) return true; } return false; } private boolean ruleMatches(String pathString, String content, ChangePlan.FileRule rule) { if (rule == null) return false; if (rule.getFilenameRegex() != null && !rule.getFilenameRegex().isBlank()) { if (!Pattern.compile(rule.getFilenameRegex()).matcher(pathString).find()) return false; } List<String> contents = rule.getContentRegexes(); if (contents != null && !contents.isEmpty()) { for (String re : contents) { if (re == null || re.isBlank()) continue; Pattern p = Pattern.compile(re, Pattern.MULTILINE | Pattern.DOTALL); if (!p.matcher(content).find()) return false; } } return true; } private String applyChanges(String original, Path path, ChangePlan plan) { List<ChangePlan.Replacement> replacements = plan.getReplacements(); String result = original; if (replacements != null && !replacements.isEmpty()) { for (ChangePlan.Replacement r : replacements) { Pattern pattern = Pattern.compile(r.getSearchRegex(), Pattern.MULTILINE | Pattern.DOTALL); result = pattern.matcher(result).replaceAll(r.getReplacement()); } return result; } // иначе используем LLM для генеративного исправления String system = """ Ты помощник по рефакторингу. Получишь инструкцию и содержимое файла, верни только новое содержимое файла (без комментариев и пояснений). Если изменений нет - верни исходное содержимое. """; String response = llmClient.transformFile(system, plan.getHighLevelInstructions() == null ? "" : plan.getHighLevelInstructions(), path.toString(), original); // На случай если модель вернет с оградителями ``` — попытаемся аккуратно извлечь String cleaned = stripCodeFences(response); return cleaned.isBlank() ? original : cleaned; } private String stripCodeFences(String text) { String trimmed = text.trim(); if (trimmed.startsWith("```") && trimmed.endsWith("```")) { String inner = trimmed.substring(3, trimmed.length() - 3); inner = inner.replaceFirst("^[a-zA-Z0-9+\\-._]+\\n", ""); return inner.trim(); } return trimmed; } private boolean isUnderGitDir(Path root, Path p) { Path relative = root.relativize(p); for (Path part : relative) { if (part.toString().equals(".git")) return true; } return false; } private boolean precheckFile(Path path) throws IOException { // Размер long size = Files.size(path); if (processingProperties.getMaxFileSizeBytes() > 0 && size > processingProperties.getMaxFileSizeBytes()) { return false; } // Бинарность if (processingProperties.isSkipBinary() && isLikelyBinary(path)) { return false; } return true; } private boolean isLikelyBinary(Path path) { try { String type = Files.probeContentType(path); if (type != null && !type.startsWith("text")) { // Для некоторых текстовых форматов probeContentType может вернуть null, поэтому только явные небуквальные типы считаем бинарем // Например application/octet-stream — бинарь if (type.equals("application/octet-stream")) return true; } } catch (IOException ignored) { } // эвристика: наличие NUL в первых 8KB byte[] buffer = new byte[8192]; try (InputStream in = Files.newInputStream(path)) { int read = in.read(buffer); int len = Math.max(read, 0); for (int i = 0; i < len; i++) { if (buffer[i] == 0) return true; } } catch (IOException ignored) { } return false; } }