feat: Allow admins to configure AI model via profile

This commit is contained in:
2025-03-26 22:37:11 +03:00
parent 202a091d6e
commit a5ec9c38d2
9 changed files with 392 additions and 114 deletions
@@ -17,12 +17,17 @@ import static ru.cathub.telegabot.utils.Constants.*;
public class KeyboardHelper {
public static ReplyKeyboardMarkup getProfileMenuKeyboard() {
// Изменена сигнатура метода
public static ReplyKeyboardMarkup getProfileMenuKeyboard(boolean isAdmin) {
List<KeyboardRow> keyboardRows = new ArrayList<>();
KeyboardRow row1 = new KeyboardRow();
row1.add(new KeyboardButton(SET_BOOKS_GOAL));
// Добавляем кнопку редактирования модели ИИ только для админа
if (isAdmin) {
row1.add(new KeyboardButton(EDIT_AI_MODEL));
}
KeyboardRow row2 = new KeyboardRow();
row2.add(new KeyboardButton(BACK));
@@ -6,21 +6,34 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
// Добавлены импорты для прокси, если они используются в бине RestTemplate
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import java.net.Proxy;
import java.net.InetSocketAddress;
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "openrouter.api")
@Setter
@Setter // <-- Аннотация @Setter осталась, она сгенерирует сеттеры для key и url
public class OpenRouterConfig {
private String key;
private String url;
private String model;
// private String model; // <-- Удалить это поле
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
// Логика создания RestTemplate с прокси или без - ВЕРНО!
// Пример с прокси:
// SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.0.5", 1984))); // Убедитесь, что адрес и порт верны
// return new RestTemplate(factory);
// Пример без прокси:
return new RestTemplate();
}
// Геттеры для key и url будут сгенерированы Lombok @Setter,
// но лучше явно добавить @Getter для них, если они нужны только для чтения внутри сервиса
public String getKey() {
return key;
}
@@ -29,7 +42,5 @@ public class OpenRouterConfig {
return url;
}
public String getModel() {
return model;
}
// Метод getModel() удален - ВЕРНО!
}
@@ -21,7 +21,9 @@ public class BotUser {
USER, ADMIN }
public enum WorkingMode {
BOOK,TORRENT,AI,EDIT_PROFILE,NO_MODE,AI_FILTER,AI_SUGGEST
BOOK, TORRENT, AI, EDIT_PROFILE, NO_MODE, AI_FILTER, AI_SUGGEST,
// Новые состояния для редактирования профиля
EDIT_PROFILE_BOOKS_GOAL, EDIT_PROFILE_AI_MODEL
}
private Integer booksToRead;
@@ -7,7 +7,11 @@ public interface ProfileService {
void enterProfileEditingMode(BotUser user);
void handleProfileUpdate(BotUser user, String input) throws ServiceException;
void handleBooksGoalUpdate(BotUser user);
// Метод для инициации запроса на изменение цели по книгам (переименован)
void handleBooksGoalUpdateInitiation(BotUser user);
// Новый метод для инициации запроса на изменение модели ИИ
void handleAiModelUpdateInitiation(BotUser user) throws ServiceException;
void handleBooksGoalShow(BotUser user);
}
@@ -4,17 +4,19 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.http.*; // Импортируем MediaType и HttpStatus
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import java.net.Proxy;
import java.net.InetSocketAddress;
// import org.springframework.http.client.SimpleClientHttpRequestFactory; // <-- Удалить, если RestTemplate инжектируется
// import java.net.Proxy; // <-- Удалить, если RestTemplate инжектируется
// import java.net.InetSocketAddress; // <-- Удалить, если RestTemplate инжектируется
import ru.cathub.telegabot.configuration.OpenRouterConfig;
import ru.cathub.telegabot.exception.OpenRouterException;
import ru.cathub.telegabot.model.TextRole;
import ru.cathub.telegabot.service.BotSettingService; // <-- Добавить импорт
import ru.cathub.telegabot.service.OpenRouterService;
import ru.cathub.telegabot.utils.Constants; // <-- Добавить импорт
import java.util.ArrayList;
import java.util.HashMap;
@@ -26,86 +28,144 @@ import java.util.Map;
@Slf4j
public class OpenRouterServiceImpl implements OpenRouterService {
private final OpenRouterConfig config;
private final RestTemplate restTemplate = createProxyRestTemplate();
private final OpenRouterConfig config; // Оставляем для URL и Key
private final RestTemplate restTemplate; // <-- Инжектируем RestTemplate из бина
private final BotSettingService botSettingService; // <-- Инжектируем сервис настроек
private RestTemplate createProxyRestTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.0.5", 1984)));
return new RestTemplate(factory);
}
// private RestTemplate createProxyRestTemplate() { // <-- Удалить этот метод
// SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.0.5", 1984)));
// return new RestTemplate(factory);
// }
@Override
public String getChatResponse(String userMessage, TextRole role) throws OpenRouterException {
List<Map<String, String>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", role.getPrompt()));
messages.add(Map.of("role", "user", "content", userMessage));
return sendChatRequest(messages);
// Передаем userMessage во второй аргумент для использования в sendChatRequest (например, для логгирования)
return sendChatRequest(messages, userMessage);
}
@Override
public String getChatResponseWithContext(String userMessage, List<Map<String, String>> context) throws OpenRouterException {
context.add(Map.of("role", "user", "content", userMessage));
return sendChatRequest(context);
// Создаем копию контекста, чтобы не модифицировать исходный список
List<Map<String, String>> messages = new ArrayList<>(context);
// Добавляем текущее сообщение пользователя в конец контекста, если его там еще нет
if (messages.isEmpty() || !messages.get(messages.size() - 1).get("role").equals("user")) {
messages.add(Map.of("role", "user", "content", userMessage));
} else {
// Если последнее сообщение уже от user, возможно, это повторный вызов, просто используем userMessage для логгирования
log.trace("Last message in context is already from user. Using provided userMessage for logging.");
}
return sendChatRequest(messages, userMessage);
}
@Override
public String getSimpleResponse(String message) throws OpenRouterException {
// Передаем message во второй аргумент для использования в sendChatRequest
return sendChatRequest(List.of(
Map.of("role", "user", "content", message)
));
), message);
}
private String sendChatRequest(List<Map<String, String>> messages) throws OpenRouterException {
// Добавляем второй аргумент для информации, которая может быть полезна для логгирования
private String sendChatRequest(List<Map<String, String>> messages, String originalUserMessage) throws OpenRouterException {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(config.getKey());
headers.add("HTTP-Referer", "https://github.com/your-username/telegabot");
headers.add("X-Title", "TelegaBot");
headers.setBearerAuth(config.getKey()); // Используем ключ из конфига
headers.setContentType(MediaType.APPLICATION_JSON); // Указываем тип контента
// Можно оставить старые заголовки или использовать новые, если нужно
headers.add("HTTP-Referer", "https://github.com/your-repo"); // Замените на ваш репозиторий
headers.add("X-Title", "TelegaBot Book Assistant"); // Замените на имя вашего бота
// Получаем имя модели из настроек бота
String currentModel = botSettingService.getSettingValueOrDefault(
Constants.AI_MODEL_SETTING_KEY,
Constants.DEFAULT_AI_MODEL // Значение по умолчанию
);
log.debug("Using AI model: {}", currentModel); // Логируем используемую модель
Map<String, Object> request = new HashMap<>();
request.put("model", config.getModel());
request.put("model", currentModel); // <-- Используем модель из настроек
request.put("messages", messages);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
try {
// Логгируем часть исходного сообщения пользователя
String messageSnippet = originalUserMessage != null ? originalUserMessage.substring(0, Math.min(originalUserMessage.length(), 100)) : "[null]";
log.info("Sending request to OpenRouter. Model: {}. Message snippet: '{}...'", currentModel, messageSnippet);
ResponseEntity<Map> response = restTemplate.postForEntity(
config.getUrl() + "/chat/completions",
new HttpEntity<>(request, headers),
config.getUrl() + "/chat/completions", // Используем URL из конфига
entity, // Используем созданный entity
Map.class
);
// Обработка ответа остается как была, но можно добавить больше логов
log.debug("Received response from OpenRouter. Status: {}", response.getStatusCode());
return extractResponse(response.getBody());
} catch (RestClientException e) {
log.error("OpenRouter API error: {}", e.getMessage());
throw new OpenRouterException("API request failed: " + e.getMessage());
log.error("OpenRouter API RestClientException: {}. URL: {}", e.getMessage(), config.getUrl(), e); // Добавляем stack trace в лог
// Можно добавить детали в сообщение об ошибке
throw new OpenRouterException("Ошибка сети при обращении к API OpenRouter: " + e.getMessage(), e);
} catch (Exception e) { // Ловим другие возможные ошибки
log.error("Unexpected error during OpenRouter API call: {}", e.getMessage(), e);
throw new OpenRouterException("Непредвиденная ошибка при вызове API OpenRouter.", e);
}
}
// Метод extractResponse остается без изменений, но с улучшенным логгированием
private String extractResponse(Map<String, Object> response) throws OpenRouterException {
try {
if (response == null) {
log.error("OpenRouter API response body is null.");
throw new OpenRouterException("API вернуло пустой ответ.");
}
if (response.containsKey("error")) {
log.error("OpenRouter API error in response: {}", response.get("error"));
throw new OpenRouterException("API request failed: " + response.get("error"));
Object errorObj = response.get("error");
String errorMessage = errorObj instanceof Map ? ((Map<?, ?>) errorObj).get("message").toString() : String.valueOf(errorObj);
log.error("OpenRouter API error in response: {}", errorMessage);
throw new OpenRouterException("API OpenRouter вернуло ошибку: " + errorMessage);
}
if (!response.containsKey("choices")) {
log.error("Invalid API response format{}", response);
throw new OpenRouterException("Invalid API response format");
log.error("Invalid API response format: 'choices' key is missing. Response: {}", response);
throw new OpenRouterException("Неверный формат ответа API: отсутствует поле 'choices'.");
}
List<?> choices = (List<?>) response.get("choices");
if (choices.isEmpty()) {
throw new OpenRouterException("No choices in API response");
log.warn("OpenRouter API response has empty 'choices' list.");
throw new OpenRouterException("API не вернуло вариантов ответа ('choices' пуст).");
}
Map<?, ?> firstChoice = (Map<?, ?>) choices.get(0);
Map<?, ?> message = (Map<?, ?>) firstChoice.get("message");
if (message == null || !message.containsKey("content")) {
throw new OpenRouterException("Malformed message in API response");
if (firstChoice == null) {
log.error("First choice in API response is null. Choices: {}", choices);
throw new OpenRouterException("Неверный формат ответа API: первый вариант ответа пуст.");
}
return (String) message.get("content");
Map<?, ?> message = (Map<?, ?>) firstChoice.get("message");
if (message == null || !message.containsKey("content")) {
log.error("Malformed message in API response: 'message' or 'content' is missing. Choice: {}", firstChoice);
throw new OpenRouterException("Неверный формат сообщения в ответе API.");
}
Object contentObj = message.get("content");
if (contentObj == null) {
log.warn("Content in API response message is null. Message: {}", message);
// Возвращаем пустую строку или выбрасываем исключение, в зависимости от ожидаемого поведения
return "";
// throw new OpenRouterException("Содержимое ответа API пусто (null).");
}
String content = (String) contentObj;
log.info("Successfully extracted content from OpenRouter response. Length: {}", content.length());
return content;
} catch (ClassCastException e) {
throw new OpenRouterException("Type casting error in API response");
log.error("Type casting error while parsing OpenRouter API response: {}", e.getMessage(), e);
throw new OpenRouterException("Ошибка при обработке ответа API (неверные типы данных).", e);
}
}
}
@@ -3,10 +3,14 @@ package ru.cathub.telegabot.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j; // Добавим логгер
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.cathub.telegabot.bot.KeyboardHelper;
import ru.cathub.telegabot.exception.ServiceException;
import ru.cathub.telegabot.model.BotUser;
import ru.cathub.telegabot.repository.UserRepository;
import ru.cathub.telegabot.service.BotSettingService; // <-- Добавить импорт
import ru.cathub.telegabot.service.ProfileService;
import ru.cathub.telegabot.service.TelegramClientService;
import ru.cathub.telegabot.utils.Constants;
@@ -16,55 +20,107 @@ import java.time.temporal.ChronoUnit;
import static ru.cathub.telegabot.bot.KeyboardHelper.getEditTargerBooksKeyboard;
import static ru.cathub.telegabot.utils.BookFormatUtils.formatBookLeftStats;
import static ru.cathub.telegabot.utils.Constants.SET_BOOKS_GOAL;
// Удаляем неиспользуемый импорт SET_BOOKS_GOAL
// import static ru.cathub.telegabot.utils.Constants.SET_BOOKS_GOAL;
import static ru.cathub.telegabot.utils.Constants.*; // Импортируем все константы
@Service
@RequiredArgsConstructor
@Slf4j // Добавляем логгер
public class ProfileServiceImpl implements ProfileService {
private final UserRepository userRepository;
private final TelegramClientService telegramClientService;
private final BotSettingService botSettingService; // <-- Добавить зависимость
@Override
public void enterProfileEditingMode(BotUser user) {
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE);
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE); // Режим выбора опции
userRepository.save(user);
boolean isAdmin = botSettingService.isAdmin(user); // <-- Проверить админа
log.debug("Entering profile edit mode for user {}. Is admin: {}", user.getId(), isAdmin);
telegramClientService.sendMessage(
user,
user,
"⌨️ Выберите параметр для редактирования:",
KeyboardHelper.getProfileMenuKeyboard()
// Передать флаг isAdmin в хелпер клавиатуры
KeyboardHelper.getProfileMenuKeyboard(isAdmin)
);
}
@Override
public void handleProfileUpdate(BotUser user, String input) throws ServiceException {
log.debug("Handling profile update for user {} in mode {}. Input: '{}'", user.getId(), user.getWorkingMode(), input);
try {
if (input.equals(SET_BOOKS_GOAL)) {
handleBooksGoalUpdate(user);
} else {
processFieldUpdate(user, input);
// Определяем, какое поле обновляем, по текущему режиму работы
switch (user.getWorkingMode()) {
case EDIT_PROFILE_BOOKS_GOAL:
processBooksGoalUpdate(user, input); // <-- Вызвать переименованный метод
break;
case EDIT_PROFILE_AI_MODEL:
processAiModelUpdate(user, input); // <-- Вызвать новый метод
break;
default:
// Если мы здесь, значит пользователь ввел текст, когда не должен был (например, в режиме EDIT_PROFILE)
log.warn("User {} provided unexpected input '{}' in mode {}", user.getId(), input, user.getWorkingMode());
boolean isAdmin = botSettingService.isAdmin(user);
telegramClientService.sendMessage(user, "Пожалуйста, выберите опцию из меню.", KeyboardHelper.getProfileMenuKeyboard(isAdmin));
// Не сбрасываем режим, даем пользователю выбрать опцию
return; // Выходим, чтобы не сбросить режим в finally
}
} finally {
resetEditingState(user);
// Сбрасываем режим только если обновление прошло успешно или была ошибка в обработке
if (user.getWorkingMode() != BotUser.WorkingMode.EDIT_PROFILE) { // Не сбрасываем, если просто показали меню
log.debug("Resetting editing state for user {}", user.getId());
resetEditingState(user);
} else {
log.debug("Not resetting state for user {} as they are still in EDIT_PROFILE mode", user.getId());
}
}
}
@Override
public void handleBooksGoalUpdate(BotUser user) {
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE);
// Переименован из handleBooksGoalUpdate
public void handleBooksGoalUpdateInitiation(BotUser user) {
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE_BOOKS_GOAL); // <-- Установить конкретный режим
userRepository.save(user);
log.info("User {} initiated books goal update.", user.getId());
telegramClientService.sendMessage(
user,
user,
"Текущая цель: " + (user.getBooksToRead() != null ? String.valueOf(user.getBooksToRead()) : "Не задано") + "\nВведите новое количество книг:"
);
}
// <-- Новый метод -->
@Override
@Transactional
public void handleAiModelUpdateInitiation(BotUser user) throws ServiceException {
if (!botSettingService.isAdmin(user)) {
log.warn("Non-admin user {} attempted to initiate AI model update.", user.getId());
throw new ServiceException("У вас нет прав для изменения этой настройки.");
}
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE_AI_MODEL); // <-- Установить конкретный режим
userRepository.save(user);
log.info("Admin user {} initiated AI model update.", user.getId());
String currentModel = botSettingService.getSettingValueOrDefault(
Constants.AI_MODEL_SETTING_KEY,
Constants.DEFAULT_AI_MODEL
);
telegramClientService.sendMessageWithMarkdown( // Используем Markdown для форматирования `code`
user,
String.format(Constants.ENTER_NEW_AI_MODEL, currentModel)
);
}
@Override
@Transactional(readOnly = true) // Добавим readOnly = true
public void handleBooksGoalShow(BotUser user) {
BotUser freshUser = userRepository.findById(user.getId()).orElseThrow();
int target = freshUser.getBooksToRead();
// Используем user напрямую, если он актуален, или получаем свежую копию
BotUser freshUser = userRepository.findById(user.getId()).orElseThrow(() -> new RuntimeException("User not found"));
Integer targetOpt = freshUser.getBooksToRead(); // Цель может быть null
int target = targetOpt != null ? targetOpt : 0; // Если null, считаем целью 0
int read = freshUser.getBooks() != null ? freshUser.getBooks().size() : 0;
int left = target - read;
@@ -74,27 +130,64 @@ public class ProfileServiceImpl implements ProfileService {
var stats = formatBookLeftStats(weeksLeft, target, read, left);
telegramClientService.sendMessageWithMarkdown(user, stats,getEditTargerBooksKeyboard());
telegramClientService.sendMessageWithMarkdown(user, stats, getEditTargerBooksKeyboard());
}
private void processFieldUpdate(BotUser user, String input) throws ServiceException {
// Переименован из processFieldUpdate
private void processBooksGoalUpdate(BotUser user, String input) throws ServiceException {
try {
int goal = Integer.parseInt(input);
if (goal < 0) throw new NumberFormatException();
int goal = Integer.parseInt(input.trim()); // Обрезаем пробелы
if (goal < 0) {
log.warn("User {} entered invalid negative goal: {}", user.getId(), goal);
throw new NumberFormatException("Goal cannot be negative");
}
user.setBooksToRead(goal);
// userRepository.save(user); // Сохранение будет в resetEditingState
log.info("User {} updated books goal to {}", user.getId(), goal);
telegramClientService.sendMessage(
user,
Constants.PROFILE_UPDATED,
KeyboardHelper.getMainMenuKeyboard()
user,
Constants.PROFILE_UPDATED,
KeyboardHelper.getMainMenuKeyboard() // Возвращаем в главное меню после успешного обновления
);
} catch (NumberFormatException e) {
throw new ServiceException("❌ Ошибка! Введите целое положительное число");
log.warn("User {} entered invalid number format for goal: {}", user.getId(), input);
throw new ServiceException("❌ Ошибка! Введите целое положительное число.");
}
}
// <-- Новый метод -->
private void processAiModelUpdate(BotUser user, String input) throws ServiceException {
// Дополнительная проверка прав не нужна, т.к. она есть в handleAiModelUpdateInitiation
// и setSettingValue внутри себя тоже проверяет права
try {
String newModel = input.trim();
if (newModel.isEmpty()) {
log.warn("Admin user {} entered empty AI model name.", user.getId());
throw new ServiceException("❌ Имя модели не может быть пустым.");
}
botSettingService.setSettingValue(Constants.AI_MODEL_SETTING_KEY, newModel, user);
log.info("Admin user {} updated AI model to '{}'", user.getId(), newModel);
telegramClientService.sendMessage(
user,
Constants.AI_MODEL_UPDATED,
KeyboardHelper.getMainMenuKeyboard() // Возвращаем в главное меню
);
} catch (ServiceException e) {
// Ловим ошибку, если вдруг setSettingValue ее выбросит (например, проблемы с БД или правами)
log.error("ServiceException while admin {} updating AI model: {}", user.getId(), e.getMessage(), e);
throw new ServiceException("❌ Не удалось обновить модель ИИ: " + e.getMessage(), e);
} catch (Exception e) {
// Ловим другие возможные ошибки
log.error("Unexpected error while admin {} updating AI model: {}", user.getId(), e.getMessage(), e);
throw new ServiceException("❌ Произошла непредвиденная ошибка при обновлении модели ИИ.", e);
}
}
private void resetEditingState(BotUser user) {
user.setWorkingMode(BotUser.WorkingMode.NO_MODE);
user.setWorkingMode(BotUser.WorkingMode.NO_MODE); // <-- Установить NO_MODE
userRepository.save(user);
log.debug("User {} state reset to NO_MODE.", user.getId());
}
}
@@ -1,35 +1,53 @@
package ru.cathub.telegabot.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; // <-- Добавить логгер
import org.springframework.stereotype.Service;
import ru.cathub.telegabot.bot.KeyboardHelper;
import ru.cathub.telegabot.exception.ServiceException;
import ru.cathub.telegabot.model.BotUser;
import ru.cathub.telegabot.repository.UserRepository;
import ru.cathub.telegabot.service.BookManageService;
import ru.cathub.telegabot.service.ProfileService;
import ru.cathub.telegabot.service.TelegramBotService;
import ru.cathub.telegabot.service.TelegramClientService;
import ru.cathub.telegabot.service.*; // <-- Импортировать все сервисы
import static ru.cathub.telegabot.utils.Constants.*;
@Service
@RequiredArgsConstructor
@Slf4j // <-- Добавить аннотацию логгера
public class TelegramBotServiceImpl implements TelegramBotService {
private final TelegramClientService telegramClientService;
private final BookManageService bookManageService;
private final ProfileService profileService;
private final UserRepository userRepository;
private final BookAiServiceImpl bookAiServiceImpl;
private final BookAiService bookAiService; // <-- Используем интерфейс
private final BotSettingService botSettingService; // <-- Добавить зависимость
@Override
public void processInput(BotUser botUser, String text) throws ServiceException {
switch (text) {
case EDIT_PROFILE:
profileService.enterProfileEditingMode(botUser);
break;
// case "Отправить текст ИИ":
log.debug("Processing input '{}' for user {} in mode {}", text, botUser.getId(), botUser.getWorkingMode());
try { // Обернем весь метод в try-catch для ServiceException
switch (text) {
case EDIT_PROFILE:
profileService.enterProfileEditingMode(botUser);
break;
// Обработка нажатия кнопок из меню профиля
case SET_BOOKS_GOAL:
profileService.handleBooksGoalUpdateInitiation(botUser);
break;
case EDIT_AI_MODEL: // <-- Добавить обработку новой кнопки
try {
profileService.handleAiModelUpdateInitiation(botUser);
} catch (ServiceException e) {
// Отправляем сообщение об ошибке прав доступа пользователю
log.warn("User {} failed to initiate AI model update: {}", botUser.getId(), e.getMessage());
telegramClientService.sendMessage(botUser, e.getMessage());
// Возвращаем пользователя в главное меню, так как он не может редактировать
processStart(botUser);
}
break;
// case "Отправить текст ИИ":
// sendMessage(chatId, "Введите ваш текст:");
// break;
//
@@ -43,55 +61,133 @@ public class TelegramBotServiceImpl implements TelegramBotService {
case "Помощь":
telegramClientService.sendMessage(botUser, "Выберите действие из меню.");
break;
bookManageService.handleStartMessage(botUser);
break;
case BACK:
processStart(botUser);
break;
case EDIT_BOOKS:
bookManageService.processBookInput(botUser,text);
break;
case SET_BOOKS_GOAL_FROM_BOOKS:
profileService.handleBooksGoalShow(botUser);
break;
default:
switch (botUser.getWorkingMode()) {
case BOOK:
bookManageService.processBookInput(botUser,text);
break;
case EDIT_PROFILE:
profileService.handleProfileUpdate(botUser, text);
break;
case AI_FILTER,AI_SUGGEST:
bookAiServiceImpl.processAiRequest(botUser,text,botUser.getWorkingMode());
break;
}
case "Помощь":
telegramClientService.sendMessage(botUser, "Выберите действие из меню.");
break;
case BACK:
log.debug("Processing BACK button for user {} in mode {}", botUser.getId(), botUser.getWorkingMode());
// Определяем, откуда был нажат BACK
if (botUser.getWorkingMode() == BotUser.WorkingMode.EDIT_PROFILE ||
botUser.getWorkingMode() == BotUser.WorkingMode.EDIT_PROFILE_AI_MODEL ||
botUser.getWorkingMode() == BotUser.WorkingMode.EDIT_PROFILE_BOOKS_GOAL) {
// Если были в редактировании профиля, возвращаемся в главное меню
log.debug("Back from profile edit, returning to main menu for user {}", botUser.getId());
processStart(botUser);
} else if (botUser.getWorkingMode() == BotUser.WorkingMode.BOOK ||
botUser.getWorkingMode() == BotUser.WorkingMode.AI_FILTER ||
botUser.getWorkingMode() == BotUser.WorkingMode.AI_SUGGEST) {
// Если были в режиме Книжника или AI, возвращаемся в меню Книжника
log.debug("Back from book/AI mode, returning to book menu for user {}", botUser.getId());
bookManageService.handleStartMessage(botUser);
}
else {
// По умолчанию возвращаемся в главное меню
log.debug("Back from unknown/NO_MODE, returning to main menu for user {}", botUser.getId());
processStart(botUser);
}
break;
case EDIT_BOOKS:
case ADD_BOOK: // Добавим обработку этих кнопок здесь, если они ведут в режим BOOK
case LIST_BOOKS:
case FILTER_BOOKS:
case RECOMMEND_BOOKS:
bookManageService.processBookInput(botUser, text);
break;
case SET_BOOKS_GOAL_FROM_BOOKS:
profileService.handleBooksGoalShow(botUser);
break;
default:
log.debug("Processing default text input for user {} in mode {}", botUser.getId(), botUser.getWorkingMode());
// Обработка ввода текста в зависимости от режима
switch (botUser.getWorkingMode()) {
case BOOK:
bookManageService.processBookInput(botUser, text);
break;
// Обработка ввода для конкретных полей профиля
case EDIT_PROFILE_BOOKS_GOAL:
case EDIT_PROFILE_AI_MODEL: // <-- Добавить обработку ввода для AI модели
profileService.handleProfileUpdate(botUser, text);
break;
case AI_FILTER:
case AI_SUGGEST:
bookAiService.processAiRequest(botUser, text, botUser.getWorkingMode());
break;
case EDIT_PROFILE:
// Пользователь ввел текст вместо нажатия кнопки в меню профиля
log.warn("User {} entered text '{}' while in EDIT_PROFILE mode", botUser.getId(), text);
boolean isAdmin = botSettingService.isAdmin(botUser);
telegramClientService.sendMessage(botUser, "Пожалуйста, выберите опцию из меню.", KeyboardHelper.getProfileMenuKeyboard(isAdmin));
break;
case NO_MODE:
default:
// Неожиданный ввод текста, когда не ожидается
log.warn("User {} entered unexpected text '{}' in mode {}", botUser.getId(), text, botUser.getWorkingMode());
telegramClientService.sendMessage(botUser, "Неизвестная команда или неверный режим. Используйте /start для возврата в главное меню.");
processStart(botUser); // Возвращаем в начало
break;
}
}
} catch (ServiceException e) {
log.error("ServiceException during processing input '{}' for user {}: {}", text, botUser.getId(), e.getMessage(), e);
telegramClientService.sendMessage(botUser, "Произошла ошибка: " + e.getMessage());
// Можно вернуть пользователя в главное меню при ошибке
processStart(botUser);
} catch (Exception e) { // Ловим остальные неожиданные ошибки
log.error("Unexpected exception during processing input '{}' for user {}: {}", text, botUser.getId(), e.getMessage(), e);
telegramClientService.sendMessage(botUser, "Произошла непредвиденная ошибка. Попробуйте позже.");
processStart(botUser);
}
}
@Override
public void processCommand(BotUser botUser, String text) {
switch (text) {
case START:
processStart(botUser);
break;
}
break;
// Можно добавить админские команды здесь, если нужно
}
}
public void processStart(BotUser botUser){
telegramClientService.sendMessage(botUser, "Добро пожаловать! Выберите действие:", KeyboardHelper.getMainMenuKeyboard());
log.info("Processing /start for user {}", botUser.getId());
// Сбрасываем режим при старте
botUser.setWorkingMode(BotUser.WorkingMode.NO_MODE);
userRepository.save(botUser);
telegramClientService.sendMessage(botUser, "Добро пожаловать! Выберите действие:", KeyboardHelper.getMainMenuKeyboard());
}
@Override
public void processQuery(BotUser botUser, String callbackData)
{
if (callbackData.startsWith("BOOK_")) {
bookManageService.processBookQuery(botUser, callbackData);
} else if (callbackData.equals("EDIT_TARGET")) {
profileService.handleBooksGoalUpdate(botUser);
log.debug("Processing query '{}' for user {}", callbackData, botUser.getId());
try { // Обернем в try-catch
if (callbackData.startsWith("BOOK_")) {
bookManageService.processBookQuery(botUser, callbackData);
} else if (callbackData.equals("EDIT_TARGET")) {
// Эта кнопка теперь инициирует редактирование цели
profileService.handleBooksGoalUpdateInitiation(botUser);
} else {
log.warn("Unknown callback query received: {} for user {}", callbackData, botUser.getId());
// Можно отправить сообщение пользователю или просто проигнорировать
// telegramClientService.sendMessage(botUser, "Неизвестный запрос.");
}
} catch (ServiceException e) {
log.error("ServiceException during processing query '{}' for user {}: {}", callbackData, botUser.getId(), e.getMessage(), e);
telegramClientService.sendMessage(botUser, "Ошибка обработки запроса: " + e.getMessage());
processStart(botUser); // Возвращаем в главное меню при ошибке
} catch (Exception e) {
log.error("Unexpected exception during processing query '{}' for user {}: {}", callbackData, botUser.getId(), e.getMessage(), e);
telegramClientService.sendMessage(botUser, "Произошла непредвиденная ошибка при обработке запроса.");
processStart(botUser);
}
}
}
@@ -47,4 +47,11 @@ public class Constants {
public static final String AI_SUGGEST_SIMILAR = "Предложи похожие книги";
public static final String AI_SUGGEST_GENRE = "Предложи еше книги в жанре фэнтези";
// AI Settings (Admin)
public static final String EDIT_AI_MODEL = "Модель ИИ";
public static final String AI_MODEL_SETTING_KEY = "OPENROUTER_API_MODEL";
public static final String DEFAULT_AI_MODEL = "google/gemini-2.5-pro-exp-03-25:free"; // Значение по умолчанию
public static final String AI_MODEL_UPDATED = "Модель ИИ обновлена! \uD83E\uDD16";
public static final String ENTER_NEW_AI_MODEL = "Текущая модель: `%s`\nВведите новое имя модели OpenRouter:"; // %s будет заменено на текущую модель
}
+2 -2
View File
@@ -29,7 +29,7 @@ logging.level.org.springframework.cache=INFO
# OpenRouter AI
openrouter.api.url=https://openrouter.ai/api/v1
openrouter.api.model=google/gemini-2.5-pro-exp-03-25:free
# openrouter.api.model=google/gemini-2.5-pro-exp-03-25:free # <-- Закомментировано или удалено
# Для безопасности лучше установить через переменную окружения:
@@ -51,4 +51,4 @@ org.jobrunr.job-scheduler.enabled=true
org.jobrunr.background-job-server.enabled=true
org.jobrunr.database.type=sql
org.jobrunr.dashboard.enabled=true
org.jobrunr.database.tablePrefix=telegabot.
org.jobrunr.database.tablePrefix=telegabot.