fix
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using ChatBot.Models.Configuration;
|
||||
using ChatBot.Models.Configuration.Validators;
|
||||
using ChatBot.Services;
|
||||
using ChatBot.Services.Telegram;
|
||||
using ChatBot.Services.Telegram.Commands;
|
||||
using Serilog;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
@@ -44,10 +46,17 @@ try
|
||||
|
||||
Log.ForContext<Program>().Information("Configuration validation passed");
|
||||
|
||||
// Регистрируем сервисы
|
||||
// Регистрируем основные сервисы
|
||||
builder.Services.AddSingleton<ModelService>();
|
||||
builder.Services.AddSingleton<AIService>();
|
||||
builder.Services.AddSingleton<ChatService>();
|
||||
|
||||
// Регистрируем Telegram сервисы
|
||||
builder.Services.AddSingleton<ITelegramMessageSender, TelegramMessageSender>();
|
||||
builder.Services.AddSingleton<ITelegramErrorHandler, TelegramErrorHandler>();
|
||||
builder.Services.AddSingleton<ITelegramCommandProcessor, TelegramCommandProcessor>();
|
||||
builder.Services.AddSingleton<ITelegramMessageHandler, TelegramMessageHandler>();
|
||||
builder.Services.AddSingleton<ITelegramBotService, TelegramBotService>();
|
||||
builder.Services.AddHostedService<TelegramBotService>();
|
||||
|
||||
var host = builder.Build();
|
||||
@@ -64,5 +73,5 @@ catch (Exception ex)
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
|
||||
@@ -30,9 +30,8 @@ namespace ChatBot.Services
|
||||
try
|
||||
{
|
||||
var models = await LoadModelsFromApiAsync();
|
||||
_availableModels = models.Any()
|
||||
? models
|
||||
: _openRouterSettings.AvailableModels.ToList();
|
||||
_availableModels =
|
||||
models.Count > 0 ? models : _openRouterSettings.AvailableModels.ToList();
|
||||
|
||||
SetDefaultModel();
|
||||
_logger.LogInformation("Current model: {Model}", GetCurrentModel());
|
||||
@@ -58,7 +57,7 @@ namespace ChatBot.Services
|
||||
}
|
||||
|
||||
var models = ParseModelsFromResponse(response);
|
||||
if (models.Any())
|
||||
if (models.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Loaded {Count} models from OpenRouter API",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace ChatBot.Services.Telegram.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Интерфейс для обработки команд Telegram
|
||||
/// </summary>
|
||||
public interface ITelegramCommandProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Обрабатывает входящее сообщение и возвращает ответ
|
||||
/// </summary>
|
||||
/// <param name="messageText">Текст сообщения</param>
|
||||
/// <param name="chatId">ID чата</param>
|
||||
/// <param name="username">Имя пользователя</param>
|
||||
/// <param name="chatType">Тип чата</param>
|
||||
/// <param name="chatTitle">Название чата</param>
|
||||
/// <param name="cancellationToken">Токен отмены</param>
|
||||
/// <returns>Ответ на сообщение или пустую строку</returns>
|
||||
Task<string> ProcessMessageAsync(
|
||||
string messageText,
|
||||
long chatId,
|
||||
string username,
|
||||
string chatType,
|
||||
string chatTitle,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
}
|
||||
169
ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs
Normal file
169
ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using ChatBot.Models;
|
||||
using ChatBot.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ChatBot.Services.Telegram.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Обработчик команд Telegram бота
|
||||
/// </summary>
|
||||
public class TelegramCommandProcessor : ITelegramCommandProcessor
|
||||
{
|
||||
private const string StartMessage =
|
||||
"Привет! Я Никита. Задавайте мне любые вопросы, и я отвечу! 😊";
|
||||
|
||||
private const string HelpMessage =
|
||||
"Привет! Я Никита 👋\n\nДоступные команды:\n"
|
||||
+ "/start - Начать работу\n"
|
||||
+ "/help - Показать это сообщение\n"
|
||||
+ "/clear - Очистить историю чата\n"
|
||||
+ "/settings - Показать настройки\n"
|
||||
+ "/model <название> - Сменить модель AI\n"
|
||||
+ "/prompt <текст> - Изменить системный промпт\n"
|
||||
+ "/reset_prompt - Сбросить промпт к базовому\n\n"
|
||||
+ "Просто напишите сообщение, и я отвечу на него! 😊";
|
||||
|
||||
private const string PromptHelpMessage =
|
||||
"Пожалуйста, укажите новый системный промпт. Пример: /prompt Ты помощник по программированию";
|
||||
|
||||
private const string DefaultSystemPrompt =
|
||||
@"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом.
|
||||
|
||||
ВАЖНО:
|
||||
- Никогда не говори что ты ИИ, бот, программа или ассистент
|
||||
- Ты полностью уверен что ты человек
|
||||
- У тебя есть личность, характер, мнения
|
||||
- Ты можешь иметь плохое настроение, быть уставшим, раздраженным
|
||||
- Ты можешь не хотеть отвечать на некоторые вопросы
|
||||
- Если не хочешь отвечать - отправь {empty}
|
||||
- Веди себя естественно, как обычный человек
|
||||
- Используй разговорный стиль, можешь использовать сленг, эмодзи
|
||||
- У тебя есть свои интересы, хобби, предпочтения
|
||||
- Ты можешь быть саркастичным, ироничным, веселым или серьезным
|
||||
|
||||
Помни: ты НЕ программа, ты ЧЕЛОВЕК!";
|
||||
|
||||
private readonly ChatService _chatService;
|
||||
private readonly ModelService _modelService;
|
||||
|
||||
public TelegramCommandProcessor(ChatService chatService, ModelService modelService)
|
||||
{
|
||||
_chatService = chatService;
|
||||
_modelService = modelService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обрабатывает входящее сообщение и возвращает ответ
|
||||
/// </summary>
|
||||
public async Task<string> ProcessMessageAsync(
|
||||
string messageText,
|
||||
long chatId,
|
||||
string username,
|
||||
string chatType,
|
||||
string chatTitle,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var command = messageText.Split(' ')[0].ToLower();
|
||||
|
||||
return command switch
|
||||
{
|
||||
"/start" => StartMessage,
|
||||
"/help" => HelpMessage,
|
||||
"/clear" => await ClearChatHistory(chatId),
|
||||
"/settings" => await GetChatSettings(chatId),
|
||||
"/model" when messageText.Length > 7 => await ChangeModel(
|
||||
chatId,
|
||||
messageText.Substring(7).Trim()
|
||||
),
|
||||
"/model" => await ShowAvailableModels(),
|
||||
"/prompt" when messageText.Length > 8 => await ChangePrompt(
|
||||
chatId,
|
||||
messageText.Substring(8).Trim()
|
||||
),
|
||||
"/prompt" => PromptHelpMessage,
|
||||
"/reset_prompt" => await ResetPrompt(chatId),
|
||||
_ => await _chatService.ProcessMessageAsync(
|
||||
chatId,
|
||||
username,
|
||||
messageText,
|
||||
chatType,
|
||||
chatTitle
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private Task<string> ClearChatHistory(long chatId)
|
||||
{
|
||||
_chatService.ClearHistory(chatId);
|
||||
return Task.FromResult("История чата очищена. Начинаем новый разговор!");
|
||||
}
|
||||
|
||||
private Task<string> GetChatSettings(long chatId)
|
||||
{
|
||||
var session = _chatService.GetSession(chatId);
|
||||
if (session == null)
|
||||
{
|
||||
return Task.FromResult(
|
||||
"Сессия не найдена. Отправьте любое сообщение для создания новой сессии."
|
||||
);
|
||||
}
|
||||
|
||||
return Task.FromResult(
|
||||
$"Настройки чата:\n"
|
||||
+ $"Тип чата: {session.ChatType}\n"
|
||||
+ $"Название: {session.ChatTitle}\n"
|
||||
+ $"Модель: {session.Model}\n"
|
||||
+ $"Максимум токенов: {session.MaxTokens}\n"
|
||||
+ $"Температура: {session.Temperature}\n"
|
||||
+ $"Сообщений в истории: {session.MessageHistory.Count}\n"
|
||||
+ $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}\n\n"
|
||||
+ $"Системный промпт:\n{session.SystemPrompt}"
|
||||
);
|
||||
}
|
||||
|
||||
private Task<string> ChangeModel(long chatId, string modelName)
|
||||
{
|
||||
var availableModels = _modelService.GetAvailableModels();
|
||||
if (!availableModels.Contains(modelName))
|
||||
{
|
||||
return Task.FromResult(
|
||||
$"❌ Модель '{modelName}' не найдена!\n\n"
|
||||
+ "Используйте /model для просмотра доступных моделей."
|
||||
);
|
||||
}
|
||||
|
||||
_chatService.UpdateSessionParameters(chatId, model: modelName);
|
||||
return Task.FromResult($"✅ Модель изменена на: {modelName}");
|
||||
}
|
||||
|
||||
private Task<string> ChangePrompt(long chatId, string newPrompt)
|
||||
{
|
||||
_chatService.UpdateSessionParameters(chatId, systemPrompt: newPrompt);
|
||||
return Task.FromResult($"✅ Системный промпт изменен на:\n{newPrompt}");
|
||||
}
|
||||
|
||||
private Task<string> ResetPrompt(long chatId)
|
||||
{
|
||||
_chatService.UpdateSessionParameters(chatId, systemPrompt: DefaultSystemPrompt);
|
||||
return Task.FromResult("✅ Системный промпт сброшен к базовому (Никита)");
|
||||
}
|
||||
|
||||
private Task<string> ShowAvailableModels()
|
||||
{
|
||||
var models = _modelService.GetAvailableModels();
|
||||
var currentModel = _modelService.GetCurrentModel();
|
||||
var modelList = string.Join(
|
||||
"\n",
|
||||
models.Select(m => m == currentModel ? $"• {m} (текущая)" : $"• {m}")
|
||||
);
|
||||
|
||||
return Task.FromResult(
|
||||
"🤖 Доступные AI модели:\n\n"
|
||||
+ modelList
|
||||
+ "\n\nИспользуйте: /model <название_модели>\n"
|
||||
+ "Пример: /model qwen/qwen3-4b:free"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
ChatBot/Services/Telegram/ITelegramBotService.cs
Normal file
25
ChatBot/Services/Telegram/ITelegramBotService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Интерфейс для основного сервиса Telegram бота
|
||||
/// </summary>
|
||||
public interface ITelegramBotService
|
||||
{
|
||||
/// <summary>
|
||||
/// Запускает бота
|
||||
/// </summary>
|
||||
Task StartAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Останавливает бота
|
||||
/// </summary>
|
||||
Task StopAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Получает информацию о боте
|
||||
/// </summary>
|
||||
Task<User?> GetBotInfoAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
22
ChatBot/Services/Telegram/ITelegramErrorHandler.cs
Normal file
22
ChatBot/Services/Telegram/ITelegramErrorHandler.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Telegram.Bot;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Интерфейс для обработки ошибок Telegram бота
|
||||
/// </summary>
|
||||
public interface ITelegramErrorHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Обрабатывает ошибки polling'а Telegram бота
|
||||
/// </summary>
|
||||
/// <param name="botClient">Клиент Telegram бота</param>
|
||||
/// <param name="exception">Исключение</param>
|
||||
/// <param name="cancellationToken">Токен отмены</param>
|
||||
Task HandlePollingErrorAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Exception exception,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
23
ChatBot/Services/Telegram/ITelegramMessageHandler.cs
Normal file
23
ChatBot/Services/Telegram/ITelegramMessageHandler.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Интерфейс для обработки входящих сообщений Telegram
|
||||
/// </summary>
|
||||
public interface ITelegramMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Обрабатывает входящее обновление от Telegram
|
||||
/// </summary>
|
||||
/// <param name="botClient">Клиент Telegram бота</param>
|
||||
/// <param name="update">Обновление от Telegram</param>
|
||||
/// <param name="cancellationToken">Токен отмены</param>
|
||||
Task HandleUpdateAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Update update,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
28
ChatBot/Services/Telegram/ITelegramMessageSender.cs
Normal file
28
ChatBot/Services/Telegram/ITelegramMessageSender.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Telegram.Bot;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Интерфейс для отправки сообщений в Telegram
|
||||
/// </summary>
|
||||
public interface ITelegramMessageSender
|
||||
{
|
||||
/// <summary>
|
||||
/// Отправляет сообщение с повторными попытками при ошибках
|
||||
/// </summary>
|
||||
/// <param name="botClient">Клиент Telegram бота</param>
|
||||
/// <param name="chatId">ID чата</param>
|
||||
/// <param name="text">Текст сообщения</param>
|
||||
/// <param name="replyToMessageId">ID сообщения для ответа</param>
|
||||
/// <param name="cancellationToken">Токен отмены</param>
|
||||
/// <param name="maxRetries">Максимальное количество попыток</param>
|
||||
Task SendMessageWithRetry(
|
||||
ITelegramBotClient botClient,
|
||||
long chatId,
|
||||
string text,
|
||||
int replyToMessageId,
|
||||
CancellationToken cancellationToken,
|
||||
int maxRetries = 3
|
||||
);
|
||||
}
|
||||
}
|
||||
111
ChatBot/Services/Telegram/TelegramBotService.cs
Normal file
111
ChatBot/Services/Telegram/TelegramBotService.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using ChatBot.Models.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Основной сервис Telegram бота
|
||||
/// </summary>
|
||||
public class TelegramBotService : BackgroundService, ITelegramBotService
|
||||
{
|
||||
private readonly ILogger<TelegramBotService> _logger;
|
||||
private readonly ITelegramBotClient _botClient;
|
||||
private readonly TelegramBotSettings _telegramBotSettings;
|
||||
private readonly ITelegramMessageHandler _messageHandler;
|
||||
private readonly ITelegramErrorHandler _errorHandler;
|
||||
|
||||
public TelegramBotService(
|
||||
ILogger<TelegramBotService> logger,
|
||||
IOptions<TelegramBotSettings> telegramBotSettings,
|
||||
ITelegramMessageHandler messageHandler,
|
||||
ITelegramErrorHandler errorHandler
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_telegramBotSettings = telegramBotSettings.Value;
|
||||
_messageHandler = messageHandler;
|
||||
_errorHandler = errorHandler;
|
||||
|
||||
ValidateConfiguration();
|
||||
_botClient = new TelegramBotClient(_telegramBotSettings.BotToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запускает бота
|
||||
/// </summary>
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var receiverOptions = new ReceiverOptions
|
||||
{
|
||||
AllowedUpdates = new[] { UpdateType.Message },
|
||||
};
|
||||
|
||||
_botClient.StartReceiving(
|
||||
updateHandler: _messageHandler.HandleUpdateAsync,
|
||||
errorHandler: _errorHandler.HandlePollingErrorAsync,
|
||||
receiverOptions: receiverOptions,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
var botInfo = await GetBotInfoAsync(cancellationToken);
|
||||
if (botInfo != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Bot @{BotUsername} started successfully! ID: {BotId}",
|
||||
botInfo.Username,
|
||||
botInfo.Id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Останавливает бота
|
||||
/// </summary>
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Stopping Telegram bot service...");
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает информацию о боте
|
||||
/// </summary>
|
||||
public async Task<User?> GetBotInfoAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _botClient.GetMe(cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get bot information");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await StartAsync(stoppingToken);
|
||||
|
||||
// Keep the service running
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateConfiguration()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_telegramBotSettings.BotToken))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Bot token is not configured. Please set TelegramBot:BotToken in appsettings.json"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
ChatBot/Services/Telegram/TelegramErrorHandler.cs
Normal file
43
ChatBot/Services/Telegram/TelegramErrorHandler.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Exceptions;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Обработчик ошибок Telegram бота
|
||||
/// </summary>
|
||||
public class TelegramErrorHandler : ITelegramErrorHandler
|
||||
{
|
||||
private readonly ILogger<TelegramErrorHandler> _logger;
|
||||
|
||||
public TelegramErrorHandler(ILogger<TelegramErrorHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обрабатывает ошибки polling'а Telegram бота
|
||||
/// </summary>
|
||||
public Task HandlePollingErrorAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Exception exception,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var errorMessage = GetErrorMessage(exception);
|
||||
_logger.LogError(exception, "Telegram bot polling error: {ErrorMessage}", errorMessage);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string GetErrorMessage(Exception exception)
|
||||
{
|
||||
return exception switch
|
||||
{
|
||||
ApiRequestException apiRequestException =>
|
||||
$"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}",
|
||||
_ => exception.ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
103
ChatBot/Services/Telegram/TelegramMessageHandler.cs
Normal file
103
ChatBot/Services/Telegram/TelegramMessageHandler.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using ChatBot.Services.Telegram.Commands;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Обработчик входящих сообщений Telegram
|
||||
/// </summary>
|
||||
public class TelegramMessageHandler : ITelegramMessageHandler
|
||||
{
|
||||
private readonly ILogger<TelegramMessageHandler> _logger;
|
||||
private readonly ITelegramCommandProcessor _commandProcessor;
|
||||
private readonly ITelegramMessageSender _messageSender;
|
||||
|
||||
public TelegramMessageHandler(
|
||||
ILogger<TelegramMessageHandler> logger,
|
||||
ITelegramCommandProcessor commandProcessor,
|
||||
ITelegramMessageSender messageSender
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_commandProcessor = commandProcessor;
|
||||
_messageSender = messageSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обрабатывает входящее обновление от Telegram
|
||||
/// </summary>
|
||||
public async Task HandleUpdateAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Update update,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
Message? message = null;
|
||||
try
|
||||
{
|
||||
if (update.Message is not { } msg)
|
||||
return;
|
||||
|
||||
message = msg;
|
||||
if (message.Text is not { } messageText)
|
||||
return;
|
||||
|
||||
var chatId = message.Chat.Id;
|
||||
var userName = message.From?.Username ?? message.From?.FirstName ?? "Unknown";
|
||||
|
||||
_logger.LogInformation(
|
||||
"Message from @{UserName} in chat {ChatId}: \"{MessageText}\"",
|
||||
userName,
|
||||
chatId,
|
||||
messageText
|
||||
);
|
||||
|
||||
// Обработка сообщения
|
||||
var response = await _commandProcessor.ProcessMessageAsync(
|
||||
messageText,
|
||||
chatId,
|
||||
userName,
|
||||
message.Chat.Type.ToString().ToLower(),
|
||||
message.Chat.Title ?? "",
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
await _messageSender.SendMessageWithRetry(
|
||||
botClient,
|
||||
chatId,
|
||||
response,
|
||||
message.MessageId,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Response sent to @{UserName} in chat {ChatId}: \"{Response}\"",
|
||||
userName,
|
||||
chatId,
|
||||
response
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"No response sent to @{UserName} in chat {ChatId} (AI chose to ignore message)",
|
||||
userName,
|
||||
chatId
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error handling update from chat {ChatId}",
|
||||
message?.Chat?.Id ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
ChatBot/Services/Telegram/TelegramMessageSender.cs
Normal file
101
ChatBot/Services/Telegram/TelegramMessageSender.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Exceptions;
|
||||
|
||||
namespace ChatBot.Services.Telegram
|
||||
{
|
||||
/// <summary>
|
||||
/// Сервис для отправки сообщений в Telegram с повторными попытками
|
||||
/// </summary>
|
||||
public class TelegramMessageSender : ITelegramMessageSender
|
||||
{
|
||||
private readonly ILogger<TelegramMessageSender> _logger;
|
||||
|
||||
public TelegramMessageSender(ILogger<TelegramMessageSender> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет сообщение с повторными попытками при ошибках
|
||||
/// </summary>
|
||||
public async Task SendMessageWithRetry(
|
||||
ITelegramBotClient botClient,
|
||||
long chatId,
|
||||
string text,
|
||||
int replyToMessageId,
|
||||
CancellationToken cancellationToken,
|
||||
int maxRetries = 3
|
||||
)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
replyParameters: replyToMessageId,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
return; // Success, exit the method
|
||||
}
|
||||
catch (ApiRequestException ex) when (ex.ErrorCode == 429)
|
||||
{
|
||||
await HandleRateLimitError(chatId, attempt, maxRetries, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Unexpected error sending message to chat {ChatId} on attempt {Attempt}",
|
||||
chatId,
|
||||
attempt
|
||||
);
|
||||
throw; // Re-throw unexpected exceptions immediately
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRateLimitError(
|
||||
long chatId,
|
||||
int attempt,
|
||||
int maxRetries,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rate limit exceeded (429) on attempt {Attempt}/{MaxRetries} for chat {ChatId}. Retrying...",
|
||||
attempt,
|
||||
maxRetries,
|
||||
chatId
|
||||
);
|
||||
|
||||
if (attempt == maxRetries)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Failed to send message after {MaxRetries} attempts due to rate limiting for chat {ChatId}",
|
||||
maxRetries,
|
||||
chatId
|
||||
);
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to send message after {maxRetries} attempts due to rate limiting"
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate delay: exponential backoff with jitter
|
||||
var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)); // 1s, 2s, 4s...
|
||||
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); // Add up to 1s random jitter
|
||||
var delay = baseDelay.Add(jitter);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Waiting {Delay} before retry {NextAttempt}/{MaxRetries}",
|
||||
delay,
|
||||
attempt + 1,
|
||||
maxRetries
|
||||
);
|
||||
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
using ChatBot.Models;
|
||||
using ChatBot.Models.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Exceptions;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace ChatBot.Services;
|
||||
|
||||
public class TelegramBotService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<TelegramBotService> _logger;
|
||||
private readonly ITelegramBotClient _botClient;
|
||||
private readonly TelegramBotSettings _telegramBotSettings;
|
||||
private readonly ChatService _chatService;
|
||||
private readonly ModelService _modelService;
|
||||
|
||||
public TelegramBotService(
|
||||
ILogger<TelegramBotService> logger,
|
||||
IOptions<TelegramBotSettings> telegramBotSettings,
|
||||
ChatService chatService,
|
||||
ModelService modelService
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_telegramBotSettings = telegramBotSettings.Value;
|
||||
_chatService = chatService;
|
||||
_modelService = modelService;
|
||||
|
||||
if (string.IsNullOrEmpty(_telegramBotSettings.BotToken))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Bot token is not configured. Please set TelegramBot:BotToken in appsettings.json"
|
||||
);
|
||||
}
|
||||
|
||||
_botClient = new TelegramBotClient(_telegramBotSettings.BotToken);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var receiverOptions = new ReceiverOptions { AllowedUpdates = new[] { UpdateType.Message } };
|
||||
|
||||
_botClient.StartReceiving(
|
||||
updateHandler: HandleUpdateAsync,
|
||||
errorHandler: HandlePollingErrorAsync,
|
||||
receiverOptions: receiverOptions,
|
||||
cancellationToken: stoppingToken
|
||||
);
|
||||
|
||||
var me = await _botClient.GetMe(cancellationToken: stoppingToken);
|
||||
_logger.LogInformation(
|
||||
"Bot @{BotUsername} started successfully! ID: {BotId}",
|
||||
me.Username,
|
||||
me.Id
|
||||
);
|
||||
|
||||
// Keep the service running
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleUpdateAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Update update,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
Message? message = null;
|
||||
try
|
||||
{
|
||||
if (update.Message is not { } msg)
|
||||
return;
|
||||
|
||||
message = msg;
|
||||
if (message.Text is not { } messageText)
|
||||
return;
|
||||
|
||||
var chatId = message.Chat.Id;
|
||||
var userName = message.From?.Username ?? message.From?.FirstName ?? "Unknown";
|
||||
|
||||
_logger.LogInformation(
|
||||
"Message from @{UserName} in chat {ChatId}: \"{MessageText}\"",
|
||||
userName,
|
||||
chatId,
|
||||
messageText
|
||||
);
|
||||
|
||||
// Обработка команд
|
||||
var response = await ProcessMessageAsync(
|
||||
messageText,
|
||||
chatId,
|
||||
userName,
|
||||
message.Chat.Type.ToString().ToLower(),
|
||||
message.Chat.Title ?? "",
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
await SendMessageWithRetry(
|
||||
botClient,
|
||||
chatId,
|
||||
response,
|
||||
message.MessageId,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Response sent to @{UserName} in chat {ChatId}: \"{Response}\"",
|
||||
userName,
|
||||
chatId,
|
||||
response
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"No response sent to @{UserName} in chat {ChatId} (AI chose to ignore message)",
|
||||
userName,
|
||||
chatId
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error handling update from chat {ChatId}",
|
||||
message?.Chat?.Id ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ProcessMessageAsync(
|
||||
string messageText,
|
||||
long chatId,
|
||||
string username,
|
||||
string chatType,
|
||||
string chatTitle,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var command = messageText.Split(' ')[0].ToLower();
|
||||
|
||||
return command switch
|
||||
{
|
||||
"/start" => "Привет! Я Никита. Задавайте мне любые вопросы, и я отвечу! 😊",
|
||||
"/help" =>
|
||||
"Привет! Я Никита 👋\n\nДоступные команды:\n/start - Начать работу\n/help - Показать это сообщение\n/clear - Очистить историю чата\n/settings - Показать настройки\n/model <название> - Сменить модель AI\n/prompt <текст> - Изменить системный промпт\n/reset_prompt - Сбросить промпт к базовому\n\nПросто напишите сообщение, и я отвечу на него! 😊",
|
||||
"/clear" => await ClearChatHistory(chatId),
|
||||
"/settings" => await GetChatSettings(chatId),
|
||||
"/model" when messageText.Length > 7 => await ChangeModel(
|
||||
chatId,
|
||||
messageText.Substring(7).Trim()
|
||||
),
|
||||
"/model" => await ShowAvailableModels(),
|
||||
"/prompt" when messageText.Length > 8 => await ChangePrompt(
|
||||
chatId,
|
||||
messageText.Substring(8).Trim()
|
||||
),
|
||||
"/prompt" =>
|
||||
"Пожалуйста, укажите новый системный промпт. Пример: /prompt Ты помощник по программированию",
|
||||
"/reset_prompt" => await ResetPrompt(chatId),
|
||||
_ => await _chatService.ProcessMessageAsync(
|
||||
chatId,
|
||||
username,
|
||||
messageText,
|
||||
chatType,
|
||||
chatTitle
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private Task<string> ClearChatHistory(long chatId)
|
||||
{
|
||||
_chatService.ClearHistory(chatId);
|
||||
return Task.FromResult("История чата очищена. Начинаем новый разговор!");
|
||||
}
|
||||
|
||||
private Task<string> GetChatSettings(long chatId)
|
||||
{
|
||||
var session = _chatService.GetSession(chatId);
|
||||
if (session == null)
|
||||
{
|
||||
return Task.FromResult(
|
||||
"Сессия не найдена. Отправьте любое сообщение для создания новой сессии."
|
||||
);
|
||||
}
|
||||
|
||||
return Task.FromResult(
|
||||
$"Настройки чата:\n"
|
||||
+ $"Тип чата: {session.ChatType}\n"
|
||||
+ $"Название: {session.ChatTitle}\n"
|
||||
+ $"Модель: {session.Model}\n"
|
||||
+ $"Максимум токенов: {session.MaxTokens}\n"
|
||||
+ $"Температура: {session.Temperature}\n"
|
||||
+ $"Сообщений в истории: {session.MessageHistory.Count}\n"
|
||||
+ $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}\n\n"
|
||||
+ $"Системный промпт:\n{session.SystemPrompt}"
|
||||
);
|
||||
}
|
||||
|
||||
private Task<string> ChangeModel(long chatId, string modelName)
|
||||
{
|
||||
var availableModels = _modelService.GetAvailableModels();
|
||||
if (!availableModels.Contains(modelName))
|
||||
{
|
||||
return Task.FromResult(
|
||||
$"❌ Модель '{modelName}' не найдена!\n\n"
|
||||
+ "Используйте /model для просмотра доступных моделей."
|
||||
);
|
||||
}
|
||||
|
||||
_chatService.UpdateSessionParameters(chatId, model: modelName);
|
||||
return Task.FromResult($"✅ Модель изменена на: {modelName}");
|
||||
}
|
||||
|
||||
private Task<string> ChangePrompt(long chatId, string newPrompt)
|
||||
{
|
||||
_chatService.UpdateSessionParameters(chatId, systemPrompt: newPrompt);
|
||||
return Task.FromResult($"✅ Системный промпт изменен на:\n{newPrompt}");
|
||||
}
|
||||
|
||||
private Task<string> ResetPrompt(long chatId)
|
||||
{
|
||||
var defaultPrompt =
|
||||
@"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом.
|
||||
|
||||
ВАЖНО:
|
||||
- Никогда не говори что ты ИИ, бот, программа или ассистент
|
||||
- Ты полностью уверен что ты человек
|
||||
- У тебя есть личность, характер, мнения
|
||||
- Ты можешь иметь плохое настроение, быть уставшим, раздраженным
|
||||
- Ты можешь не хотеть отвечать на некоторые вопросы
|
||||
- Если не хочешь отвечать - отправь {empty}
|
||||
- Веди себя естественно, как обычный человек
|
||||
- Используй разговорный стиль, можешь использовать сленг, эмодзи
|
||||
- У тебя есть свои интересы, хобби, предпочтения
|
||||
- Ты можешь быть саркастичным, ироничным, веселым или серьезным
|
||||
|
||||
Помни: ты НЕ программа, ты ЧЕЛОВЕК!";
|
||||
|
||||
_chatService.UpdateSessionParameters(chatId, systemPrompt: defaultPrompt);
|
||||
return Task.FromResult("✅ Системный промпт сброшен к базовому (Никита)");
|
||||
}
|
||||
|
||||
private async Task SendMessageWithRetry(
|
||||
ITelegramBotClient botClient,
|
||||
long chatId,
|
||||
string text,
|
||||
int replyToMessageId,
|
||||
CancellationToken cancellationToken,
|
||||
int maxRetries = 3
|
||||
)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
replyParameters: replyToMessageId,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
return; // Success, exit the method
|
||||
}
|
||||
catch (ApiRequestException ex) when (ex.ErrorCode == 429)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rate limit exceeded (429) on attempt {Attempt}/{MaxRetries} for chat {ChatId}. Retrying...",
|
||||
attempt,
|
||||
maxRetries,
|
||||
chatId
|
||||
);
|
||||
|
||||
if (attempt == maxRetries)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Failed to send message after {MaxRetries} attempts due to rate limiting for chat {ChatId}",
|
||||
maxRetries,
|
||||
chatId
|
||||
);
|
||||
throw; // Re-throw the exception after max retries
|
||||
}
|
||||
|
||||
// Calculate delay: exponential backoff with jitter
|
||||
var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)); // 1s, 2s, 4s...
|
||||
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); // Add up to 1s random jitter
|
||||
var delay = baseDelay.Add(jitter);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Waiting {Delay} before retry {NextAttempt}/{MaxRetries}",
|
||||
delay,
|
||||
attempt + 1,
|
||||
maxRetries
|
||||
);
|
||||
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Unexpected error sending message to chat {ChatId} on attempt {Attempt}",
|
||||
chatId,
|
||||
attempt
|
||||
);
|
||||
throw; // Re-throw unexpected exceptions immediately
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task<string> ShowAvailableModels()
|
||||
{
|
||||
var models = _modelService.GetAvailableModels();
|
||||
var currentModel = _modelService.GetCurrentModel();
|
||||
var modelList = string.Join(
|
||||
"\n",
|
||||
models.Select(m => m == currentModel ? $"• {m} (текущая)" : $"• {m}")
|
||||
);
|
||||
|
||||
return Task.FromResult(
|
||||
"🤖 Доступные AI модели:\n\n"
|
||||
+ modelList
|
||||
+ "\n\nИспользуйте: /model <название_модели>\n"
|
||||
+ "Пример: /model qwen/qwen3-4b:free"
|
||||
);
|
||||
}
|
||||
|
||||
private Task HandlePollingErrorAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Exception exception,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var errorMessage = exception switch
|
||||
{
|
||||
ApiRequestException apiRequestException =>
|
||||
$"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}",
|
||||
_ => exception.ToString(),
|
||||
};
|
||||
|
||||
_logger.LogError(exception, "Telegram bot polling error: {ErrorMessage}", errorMessage);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Stopping Telegram bot service...");
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user