фикс с разными тгк
This commit is contained in:
parent
b861e99ce2
commit
2553b13367
15
Configuration/ScheduleEntry.cs
Normal file
15
Configuration/ScheduleEntry.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace DailyDigestWorker.Configuration
|
||||
{
|
||||
// Описывает одну запись в расписании
|
||||
public class ScheduleEntry
|
||||
{
|
||||
// Время запуска (ваше локальное) в формате "HH:mm:ss"
|
||||
public required string TimeLocal { get; set; }
|
||||
|
||||
// Юзернейм канала Telegram (без @) для этого времени
|
||||
public required string ChannelUsername { get; set; }
|
||||
|
||||
// Опционально: можно добавить другие параметры, специфичные для этого запуска
|
||||
// public string? CustomPromptSuffix { get; set; }
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
namespace DailyDigestWorker.Configuration
|
||||
using System.Collections.Generic; // Для List
|
||||
|
||||
namespace DailyDigestWorker.Configuration
|
||||
{
|
||||
public class SchedulingSettings
|
||||
{
|
||||
// Список времен запуска в формате "HH:mm:ss" (это ВАШЕ локальное время)
|
||||
public List<string>? RunAtTimesLocal { get; set; }
|
||||
// Новый список: содержит объекты с временем и каналом
|
||||
public List<ScheduleEntry>? Schedules { get; set; }
|
||||
|
||||
// Идентификатор целевого часового пояса (IANA ID)
|
||||
public string TargetTimeZoneId { get; set; } = "Asia/Novokuznetsk"; // Значение по умолчанию
|
||||
// Идентификатор целевого часового пояса остается
|
||||
public string TargetTimeZoneId { get; set; } = "UTC"; // Безопасное значение по умолчанию
|
||||
}
|
||||
}
|
@ -1,19 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DailyDigestWorker.Services
|
||||
namespace DailyDigestWorker.Services
|
||||
{
|
||||
// Интерфейс для сервиса чтения сообщений из Telegram-канала
|
||||
public interface ITelegramChannelReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает тексты последних сообщений из целевого канала.
|
||||
/// </summary>
|
||||
/// <param name="maxAgeHours">Максимальный возраст сообщений в часах.</param>
|
||||
/// <param name="limit">Максимальное количество сообщений для проверки.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Список текстов сообщений (от старых к новым) или null в случае ошибки.</returns>
|
||||
Task<List<string>?> GetRecentNewsAsync(int maxAgeHours, int limit, CancellationToken cancellationToken);
|
||||
// Добавляем параметр channelUsername
|
||||
Task<List<string>?> GetRecentNewsAsync(
|
||||
string channelUsername, // <-- Имя канала для этого конкретного запуска
|
||||
int maxAgeHours,
|
||||
int limit,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -1,94 +1,144 @@
|
||||
using DailyDigestWorker.Configuration; // Для TelegramClientSettings
|
||||
using Microsoft.Extensions.Options; // Для IOptions
|
||||
using TL; // Основное пространство имен WTelegramClient для типов Telegram API
|
||||
using DailyDigestWorker.Configuration; // Не используется напрямую, но нужно для DI WTelegramClient
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TL; // Основное пространство имен WTelegramClient
|
||||
using WTelegram; // Для Client
|
||||
|
||||
namespace DailyDigestWorker.Services
|
||||
{
|
||||
public class TelegramChannelReader : ITelegramChannelReader // Реализуем интерфейс
|
||||
public class TelegramChannelReader : ITelegramChannelReader // Реализуем обновленный интерфейс
|
||||
{
|
||||
private readonly ILogger<TelegramChannelReader> _logger;
|
||||
private readonly Client _client; // Наш Singleton WTelegramClient
|
||||
private readonly TelegramClientSettings _settings;
|
||||
private InputPeer? _targetPeer; // Кэшированный InputPeer канала
|
||||
// Зависимость от IOptions<TelegramClientSettings> убрана
|
||||
// Кэшированное поле _targetPeer убрано
|
||||
|
||||
// Внедряем зависимости
|
||||
// Конструктор теперь принимает только логгер и WTelegramClient
|
||||
public TelegramChannelReader(
|
||||
ILogger<TelegramChannelReader> logger,
|
||||
Client client, // Внедряем WTelegramClient
|
||||
IOptions<TelegramClientSettings> settingsOptions)
|
||||
Client client)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = client;
|
||||
_settings = settingsOptions.Value;
|
||||
}
|
||||
|
||||
public async Task<List<string>?> GetRecentNewsAsync(int maxAgeHours, int limit, CancellationToken cancellationToken)
|
||||
// Метод теперь принимает channelUsername как параметр
|
||||
public async Task<List<string>?> GetRecentNewsAsync(
|
||||
string channelUsername, // Имя канала для этого конкретного вызова
|
||||
int maxAgeHours,
|
||||
int limit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Попытка чтения новостей из канала {ChannelUsername}...", _settings.TargetChannelUsername);
|
||||
|
||||
// Внешний try-catch для диагностики любых ошибок
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Шаг 1: Проверка/выполнение авторизации пользователя...");
|
||||
// Добавляем try-catch вокруг LoginUserIfNeeded для более детального логгирования
|
||||
_logger.LogInformation("[GetRecentNewsAsync] Попытка чтения новостей из канала {ChannelUsername}...", channelUsername);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogWarning("[GetRecentNewsAsync] Операция отменена ПЕРЕД началом работы для канала {ChannelUsername}.", channelUsername);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogDebug("[GetRecentNewsAsync] Шаг 1: Проверка/выполнение авторизации пользователя...");
|
||||
User? currentUser = null;
|
||||
try
|
||||
{
|
||||
currentUser = await _client.LoginUserIfNeeded(); // Может запросить ввод в консоль!
|
||||
// Выполняем логин, если необходимо (может запросить ввод в консоль)
|
||||
currentUser = await _client.LoginUserIfNeeded().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception loginEx)
|
||||
{
|
||||
_logger.LogError(loginEx, "Ошибка во время выполнения LoginUserIfNeeded.");
|
||||
return null; // Выходим, если логин не удался
|
||||
_logger.LogError(loginEx, "[GetRecentNewsAsync] Ошибка ВО ВРЕМЯ выполнения LoginUserIfNeeded.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentUser == null)
|
||||
{
|
||||
_logger.LogError("Авторизация не удалась (LoginUserIfNeeded вернул null или выбросил исключение).");
|
||||
_logger.LogError("[GetRecentNewsAsync] Авторизация не удалась (LoginUserIfNeeded вернул null или произошла ошибка выше).");
|
||||
return null;
|
||||
}
|
||||
_logger.LogInformation("Шаг 1 Успех: Авторизация как {UserFirstName} (ID: {UserId})", currentUser.first_name, currentUser.id);
|
||||
_logger.LogInformation("[GetRecentNewsAsync] Шаг 1 Успех: Авторизация как {UserFirstName} (ID: {UserId})", currentUser.first_name, currentUser.id);
|
||||
|
||||
|
||||
// 2. Найти канал (получить InputPeer) - кэшируем результат
|
||||
if (_targetPeer == null)
|
||||
// 2. Найти канал (получить InputPeer) - делаем это каждый раз
|
||||
_logger.LogDebug("[GetRecentNewsAsync] Шаг 2: Поиск InputPeer для канала {ChannelUsername}...", channelUsername);
|
||||
InputPeer? targetPeer = null; // Локальная переменная
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Шаг 2: Поиск InputPeer для канала {ChannelUsername}...", _settings.TargetChannelUsername);
|
||||
var resolved = await _client.Contacts_ResolveUsername(_settings.TargetChannelUsername);
|
||||
var resolved = await _client.Contacts_ResolveUsername(channelUsername).ConfigureAwait(false);
|
||||
if (resolved?.UserOrChat is Channel channel)
|
||||
{
|
||||
_targetPeer = channel.ToInputPeer();
|
||||
_logger.LogInformation("Шаг 2 Успех: Найден InputPeer для канала '{ChannelTitle}' (ID: {ChannelId})", channel.title, channel.id);
|
||||
targetPeer = channel.ToInputPeer();
|
||||
_logger.LogInformation("[GetRecentNewsAsync] Шаг 2 Успех: Найден InputPeer для канала '{ChannelTitle}' (ID: {ChannelId})", channel.title, channel.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Шаг 2 Ошибка: Не удалось найти канал по юзернейму {ChannelUsername} или это не канал. Resolved: {@Resolved}", _settings.TargetChannelUsername, resolved);
|
||||
_logger.LogError("[GetRecentNewsAsync] Шаг 2 Ошибка: Не удалось найти канал по юзернейму {ChannelUsername} или это не канал. Resolved: {@Resolved}", channelUsername, resolved);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (RpcException e)
|
||||
{
|
||||
_logger.LogDebug("Шаг 2: Используется кэшированный InputPeer для канала.");
|
||||
}
|
||||
|
||||
// 3. Получить историю сообщений
|
||||
_logger.LogDebug("Шаг 3: Запрос истории сообщений (limit={Limit})...", limit);
|
||||
var history = await _client.Messages_GetHistory(_targetPeer, limit: limit);
|
||||
if (history == null)
|
||||
{
|
||||
_logger.LogWarning("Шаг 3 Ошибка: Не удалось получить историю сообщений (результат null).");
|
||||
_logger.LogError(e, "[GetRecentNewsAsync] Ошибка Telegram API ({ErrorCode}) при поиске канала {ChannelUsername}: {ErrorMessage}", e.Code, channelUsername, e.Message);
|
||||
return null;
|
||||
}
|
||||
_logger.LogInformation("Шаг 3 Успех: Получено {Count} сообщений/элементов в истории.", history.Messages.Length);
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[GetRecentNewsAsync] Неожиданная ошибка при поиске канала {ChannelUsername}.", channelUsername);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Если targetPeer все еще null (не должно произойти при успешном resolve)
|
||||
if (targetPeer == null)
|
||||
{
|
||||
_logger.LogError("[GetRecentNewsAsync] Критическая ошибка: InputPeer остался null после ResolveUsername для {ChannelUsername}", channelUsername);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 3. Получить историю сообщений
|
||||
_logger.LogDebug("[GetRecentNewsAsync] Шаг 3: Запрос истории сообщений (limit={Limit})...", limit);
|
||||
Messages_MessagesBase? history = null; // Используем nullable тип
|
||||
try
|
||||
{
|
||||
history = await _client.Messages_GetHistory(targetPeer, limit: limit).ConfigureAwait(false);
|
||||
}
|
||||
catch (RpcException e)
|
||||
{
|
||||
_logger.LogError(e, "[GetRecentNewsAsync] Ошибка Telegram API ({ErrorCode}) при получении истории для {ChannelUsername}: {ErrorMessage}", e.Code, channelUsername, e.Message);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[GetRecentNewsAsync] Неожиданная ошибка при получении истории для {ChannelUsername}.", channelUsername);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (history == null)
|
||||
{
|
||||
_logger.LogWarning("[GetRecentNewsAsync] Шаг 3 Ошибка: Не удалось получить историю сообщений (результат null) для канала {ChannelUsername}.", channelUsername);
|
||||
return null;
|
||||
}
|
||||
_logger.LogInformation("[GetRecentNewsAsync] Шаг 3 Успех: Получено {Count} сообщений/элементов в истории для канала {ChannelUsername}.", history.Messages.Length, channelUsername);
|
||||
|
||||
|
||||
// 4. Отфильтровать сообщения по дате и извлечь текст
|
||||
_logger.LogDebug("Шаг 4: Фильтрация сообщений новее {CutoffDateUtc} UTC...", DateTime.UtcNow.AddHours(-maxAgeHours));
|
||||
_logger.LogDebug("[GetRecentNewsAsync] Шаг 4: Фильтрация сообщений новее {CutoffDateUtc} UTC...", DateTime.UtcNow.AddHours(-maxAgeHours));
|
||||
var newsTexts = new List<string>();
|
||||
var cutoffDate = DateTime.UtcNow.AddHours(-maxAgeHours);
|
||||
|
||||
foreach (var msgBase in history.Messages)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogWarning("[GetRecentNewsAsync] Операция отменена во время фильтрации сообщений.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (msgBase is Message message)
|
||||
{
|
||||
if (message.Date >= cutoffDate)
|
||||
@ -96,35 +146,34 @@ namespace DailyDigestWorker.Services
|
||||
if (!string.IsNullOrWhiteSpace(message.message))
|
||||
{
|
||||
newsTexts.Add(message.message);
|
||||
// Уменьшим детальность этого лога, чтобы не засорять вывод
|
||||
// _logger.LogDebug(" + Добавлено сообщение от {MessageDate:yyyy-MM-dd HH:mm} UTC (ID: {MessageId})", message.Date, message.id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Достигнуто сообщение старше {CutoffDateUtc} UTC, остановка фильтрации.", cutoffDate);
|
||||
break;
|
||||
_logger.LogDebug("[GetRecentNewsAsync] Достигнуто сообщение старше {CutoffDateUtc} UTC, остановка фильтрации.", cutoffDate);
|
||||
break; // Оптимизация: сообщения идут от новых к старым
|
||||
}
|
||||
}
|
||||
}
|
||||
_logger.LogInformation("Шаг 4 Успех: Найдено {Count} новостных сообщений за последние {Hours} часов.", newsTexts.Count, maxAgeHours);
|
||||
_logger.LogInformation("[GetRecentNewsAsync] Шаг 4 Успех: Найдено {Count} новостных сообщений за последние {Hours} часов из канала {ChannelUsername}.", newsTexts.Count, maxAgeHours, channelUsername);
|
||||
|
||||
// Сообщения были от новых к старым, переворачиваем для хронологии
|
||||
newsTexts.Reverse();
|
||||
return newsTexts;
|
||||
|
||||
}
|
||||
// ... (остальные catch блоки без изменений) ...
|
||||
catch (RpcException e)
|
||||
catch (RpcException e) // Ловим ошибки Telegram API на уровне всего метода
|
||||
{
|
||||
_logger.LogError(e, "Ошибка Telegram API ({ErrorCode}): {ErrorMessage}", e.Code, e.Message);
|
||||
if (e.Code == 420)
|
||||
_logger.LogError(e, "[GetRecentNewsAsync] Ошибка Telegram API ({ErrorCode}) во время работы с каналом {ChannelUsername}: {ErrorMessage}", e.Code, channelUsername, e.Message);
|
||||
if (e.Code == 420) // FLOOD_WAIT_X
|
||||
{
|
||||
_logger.LogWarning("Получен FloodWait на {Seconds} секунд. Пропускаем цикл чтения новостей.", e.X);
|
||||
_logger.LogWarning("[GetRecentNewsAsync] Получен FloodWait на {Seconds} секунд. Пропускаем чтение.", e.X);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception ex) // Ловим ЛЮБЫЕ другие ошибки внутри метода
|
||||
{
|
||||
_logger.LogError(ex, "Неожиданная ошибка при чтении новостей из Telegram канала.");
|
||||
_logger.LogError(ex, "[GetRecentNewsAsync] Неожиданная ошибка ВНУТРИ метода для канала {ChannelUsername}.", channelUsername);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
196
Worker.cs
196
Worker.cs
@ -6,11 +6,14 @@ using Telegram.Bot.Types.Enums; // Для ParseMode
|
||||
|
||||
namespace DailyDigestWorker
|
||||
{
|
||||
// Структура для хранения распарсенного расписания (внутренняя)
|
||||
internal readonly record struct ParsedScheduleEntry(TimeSpan TimeLocal, string ChannelUsername, DateTime NextRunTargetZone);
|
||||
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
// Список времен запуска в формате TimeSpan (представляют ЛОКАЛЬНОЕ время ЦЕЛЕВОГО часового пояса)
|
||||
private readonly List<TimeSpan> _runTimesLocal;
|
||||
// Храним список пар (Время в целевой зоне, Имя канала)
|
||||
private readonly List<(TimeSpan TimeLocal, string ChannelUsername)> _schedules;
|
||||
private readonly TimeZoneInfo _targetTimeZone; // Целевой часовой пояс пользователя
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
@ -20,12 +23,11 @@ namespace DailyDigestWorker
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_runTimesLocal = new List<TimeSpan>(); // Инициализируем список
|
||||
_schedules = new List<(TimeSpan TimeLocal, string ChannelUsername)>(); // Инициализируем список
|
||||
|
||||
// Получаем настройки планировщика (или используем дефолтные, если секция отсутствует)
|
||||
var settings = schedulingSettings.Value ?? new SchedulingSettings();
|
||||
var timeStrings = settings.RunAtTimesLocal; // Список строк времени
|
||||
string targetTimeZoneId = settings.TargetTimeZoneId; // ID часового пояса
|
||||
var scheduleEntries = settings.Schedules; // Получаем новый список ScheduleEntry
|
||||
string targetTimeZoneId = settings.TargetTimeZoneId;
|
||||
|
||||
// 1. Определяем целевой часовой пояс
|
||||
try
|
||||
@ -36,77 +38,86 @@ namespace DailyDigestWorker
|
||||
catch (TimeZoneNotFoundException)
|
||||
{
|
||||
_logger.LogError("Часовой пояс с ID '{TimeZoneId}' не найден в системе! Будет использовано локальное время сервера (TimeZoneInfo.Local).", targetTimeZoneId);
|
||||
_targetTimeZone = TimeZoneInfo.Local; // Откат к локальному времени сервера
|
||||
_targetTimeZone = TimeZoneInfo.Local;
|
||||
}
|
||||
catch (InvalidTimeZoneException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка при загрузке данных часового пояса '{TimeZoneId}'. Будет использовано локальное время сервера.", targetTimeZoneId);
|
||||
_targetTimeZone = TimeZoneInfo.Local;
|
||||
}
|
||||
catch (Exception ex) // Ловим другие возможные ошибки при работе с TimeZoneInfo
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Неожиданная ошибка при определении часового пояса '{TimeZoneId}'. Будет использовано локальное время сервера.", targetTimeZoneId);
|
||||
_targetTimeZone = TimeZoneInfo.Local;
|
||||
}
|
||||
|
||||
|
||||
// 2. Парсим ВРЕМЕНА ЗАПУСКА (они всегда в целевом часовом поясе)
|
||||
if (timeStrings != null)
|
||||
// 2. Парсим новое расписание из конфигурации
|
||||
if (scheduleEntries != null)
|
||||
{
|
||||
foreach (var timeStr in timeStrings)
|
||||
foreach (var entry in scheduleEntries)
|
||||
{
|
||||
// Используем ParseExact для строгого формата "HH:mm:ss"
|
||||
if (TimeSpan.TryParseExact(timeStr?.Trim(), @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var timeSpan))
|
||||
if (entry != null &&
|
||||
!string.IsNullOrWhiteSpace(entry.ChannelUsername) &&
|
||||
TimeSpan.TryParseExact(entry.TimeLocal?.Trim(), @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var timeSpan))
|
||||
{
|
||||
_runTimesLocal.Add(timeSpan);
|
||||
// Добавляем валидную пару (время, канал) в список
|
||||
_schedules.Add((timeSpan, entry.ChannelUsername.Trim())); // Trim на случай лишних пробелов
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Не удалось распознать время '{TimeString}' в настройках Scheduling:RunAtTimesLocal. Используйте формат HH:mm:ss.", timeStr);
|
||||
_logger.LogWarning("Некорректная или неполная запись в расписании Scheduling:Schedules: {@ScheduleEntry}. Запись проигнорирована.", entry);
|
||||
}
|
||||
}
|
||||
// Убираем дубликаты и сортируем
|
||||
_runTimesLocal = _runTimesLocal.Distinct().OrderBy(t => t).ToList();
|
||||
// Убираем дубликаты и сортируем по времени
|
||||
_schedules = _schedules
|
||||
.GroupBy(s => s.TimeLocal) // Группируем по времени, чтобы избежать дублей времени
|
||||
.Select(g => g.First()) // Берем первую запись для каждого времени
|
||||
.OrderBy(s => s.TimeLocal) // Сортируем по времени
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// 3. Проверка и логирование времен запуска
|
||||
if (!_runTimesLocal.Any())
|
||||
// 3. Проверка и логирование расписания
|
||||
if (!_schedules.Any())
|
||||
{
|
||||
_logger.LogError("В конфигурации не указано ни одного корректного времени запуска (RunAtTimesLocal)! Worker не сможет автоматически запускаться.");
|
||||
// Здесь НЕТ добавления времени по умолчанию, чтобы требовать явную конфигурацию
|
||||
_logger.LogError("В конфигурации не указано ни одного корректного расписания (Scheduling:Schedules)! Worker не сможет автоматически запускаться.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Worker инициализирован. Дайджест будет запускаться ежедневно в (по времени {TargetTimeZoneId}): {RunTimes}",
|
||||
_targetTimeZone.Id,
|
||||
_runTimesLocal.Any() ? string.Join(", ", _runTimesLocal.Select(t => t.ToString(@"hh\:mm\:ss"))) : "НЕТ ВРЕМЕН ЗАПУСКА");
|
||||
_logger.LogInformation("Worker инициализирован. Расписание запусков (по времени {TargetTimeZoneId}):", _targetTimeZone.Id);
|
||||
if (_schedules.Any())
|
||||
{
|
||||
foreach (var schedule in _schedules)
|
||||
{
|
||||
_logger.LogInformation(" - {Time} -> Канал: {Channel}", schedule.TimeLocal.ToString(@"hh\:mm\:ss"), schedule.ChannelUsername);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(" - НЕТ ЗАПЛАНИРОВАННЫХ ЗАПУСКОВ");
|
||||
}
|
||||
}
|
||||
|
||||
// Основной метод, который выполняется в фоне
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Worker запущен в: {time} UTC", DateTimeOffset.UtcNow); // Логгируем UTC время запуска
|
||||
_logger.LogInformation("Worker запущен в: {time} UTC", DateTimeOffset.UtcNow);
|
||||
|
||||
if (!_runTimesLocal.Any())
|
||||
if (!_schedules.Any())
|
||||
{
|
||||
_logger.LogError("Нет сконфигурированных времен запуска. Worker завершает работу.");
|
||||
return; // Останавливаем выполнение, если нет времен
|
||||
_logger.LogError("Нет сконфигурированных расписаний запуска. Worker завершает работу.");
|
||||
return; // Останавливаем выполнение, если нет расписаний
|
||||
}
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
// Рассчитываем задержку до ближайшего времени запуска в целевом часовом поясе
|
||||
TimeSpan delay = CalculateDelayUntilNearestRunInTargetZone(_runTimesLocal, _targetTimeZone);
|
||||
// Рассчитываем задержку до ближайшего запуска И получаем задание для этого запуска
|
||||
(TimeSpan delay, string channelForNextRun, DateTime nextRunTargetZone) = CalculateDelayAndNextSchedule(_schedules, _targetTimeZone);
|
||||
|
||||
// Логируем время следующего запуска в UTC и в целевой зоне
|
||||
var nextRunUtc = DateTime.UtcNow.Add(delay);
|
||||
var nextRunTargetZone = TimeZoneInfo.ConvertTimeFromUtc(nextRunUtc, _targetTimeZone);
|
||||
|
||||
_logger.LogInformation("Следующий запуск дайджеста запланирован через: {Delay} (в {TargetTimeLocal} {TargetZoneId})",
|
||||
delay, nextRunTargetZone.ToString("yyyy-MM-dd HH:mm:ss"), _targetTimeZone.Id);
|
||||
_logger.LogInformation("Следующий запуск дайджеста из канала '{Channel}' запланирован через: {Delay} (в {TargetTimeLocal} {TargetZoneId})",
|
||||
channelForNextRun, delay, nextRunTargetZone.ToString("yyyy-MM-dd HH:mm:ss"), _targetTimeZone.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Ожидаем до следующего времени запуска
|
||||
// Ожидаем рассчитанную задержку
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@ -117,11 +128,16 @@ namespace DailyDigestWorker
|
||||
|
||||
if (stoppingToken.IsCancellationRequested) break; // Проверяем еще раз
|
||||
|
||||
// --- Определяем задание для ТЕКУЩЕГО запуска ---
|
||||
// Пересчитываем, чтобы получить актуальное задание на момент пробуждения
|
||||
(_, string currentChannelUsername, _) = CalculateDelayAndNextSchedule(_schedules, _targetTimeZone);
|
||||
_logger.LogInformation("--- Начало цикла обработки дайджеста для канала: [{ChannelUsername}] ({Time} UTC) ---", currentChannelUsername, DateTimeOffset.UtcNow);
|
||||
|
||||
|
||||
// --- Выполнение основной задачи ---
|
||||
_logger.LogInformation("Запуск процесса создания дайджеста в: {time} UTC", DateTimeOffset.UtcNow);
|
||||
using (var scope = _serviceProvider.CreateScope())
|
||||
{
|
||||
// --- Получаем сервисы ---
|
||||
// Получаем все необходимые сервисы
|
||||
var currencyService = scope.ServiceProvider.GetRequiredService<ICurrencyService>();
|
||||
var cryptoService = scope.ServiceProvider.GetRequiredService<ICryptoService>();
|
||||
var weatherService = scope.ServiceProvider.GetRequiredService<IWeatherService>();
|
||||
@ -130,18 +146,19 @@ namespace DailyDigestWorker
|
||||
var newsReaderService = scope.ServiceProvider.GetRequiredService<ITelegramChannelReader>();
|
||||
var summarizationService = scope.ServiceProvider.GetRequiredService<ISummarizationService>();
|
||||
|
||||
const int NewsMaxAgeHours = 24;
|
||||
const int NewsMaxAgeHours = 24; // Можно сделать динамическим
|
||||
const int NewsFetchLimit = 50;
|
||||
string? newsSummary = null;
|
||||
|
||||
try
|
||||
{
|
||||
// --- 1. Сбор данных ---
|
||||
_logger.LogInformation("Начало сбора данных для дайджеста...");
|
||||
_logger.LogInformation("1. Начало сбора данных...");
|
||||
var currencyTask = currencyService.GetCurrencyRatesAsync(stoppingToken);
|
||||
var cryptoTask = cryptoService.GetCryptoRatesAsync(stoppingToken);
|
||||
var weatherTask = weatherService.GetWeatherAsync(stoppingToken);
|
||||
var newsTask = newsReaderService.GetRecentNewsAsync(NewsMaxAgeHours, NewsFetchLimit, stoppingToken);
|
||||
// Передаем имя канала, актуальное для ЭТОГО запуска
|
||||
var newsTask = newsReaderService.GetRecentNewsAsync(currentChannelUsername, NewsMaxAgeHours, NewsFetchLimit, stoppingToken);
|
||||
|
||||
await Task.WhenAll(currencyTask, cryptoTask, weatherTask, newsTask);
|
||||
|
||||
@ -149,65 +166,59 @@ namespace DailyDigestWorker
|
||||
var cryptoData = await cryptoTask;
|
||||
var weatherData = await weatherTask;
|
||||
var rawNewsTexts = await newsTask;
|
||||
_logger.LogInformation("Сбор всех базовых данных завершен.");
|
||||
_logger.LogInformation("1. Сбор данных завершен.");
|
||||
|
||||
// --- 1.5 Генерация саммари ---
|
||||
// --- 1.5 Суммаризация ---
|
||||
if (rawNewsTexts != null && rawNewsTexts.Any())
|
||||
{
|
||||
_logger.LogInformation("Получено {NewsCount} текстов новостей. Запуск суммаризации...", rawNewsTexts.Count);
|
||||
_logger.LogInformation("1.5 Получено {NewsCount} текстов из '{Channel}'. Запуск суммаризации...", rawNewsTexts.Count, currentChannelUsername);
|
||||
try
|
||||
{
|
||||
string combinedNewsText = string.Join("\n\n---\n\n", rawNewsTexts);
|
||||
// В будущем можно передать currentChannelUsername в SummarizeTextAsync для контекста промпта
|
||||
newsSummary = await summarizationService.SummarizeTextAsync(combinedNewsText, stoppingToken);
|
||||
if (newsSummary != null)
|
||||
{
|
||||
_logger.LogInformation("Саммари новостей успешно получено.");
|
||||
_logger.LogDebug("Текст саммари:\n{NewsSummary}", newsSummary);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Сервис суммаризации вернул null.");
|
||||
}
|
||||
|
||||
if (newsSummary != null) { _logger.LogInformation("1.5 Суммаризация успешна."); }
|
||||
else { _logger.LogWarning("1.5 Сервис суммаризации вернул null."); }
|
||||
}
|
||||
catch (OperationCanceledException) { _logger.LogInformation("Операция суммаризации была отменена."); }
|
||||
catch (Exception ex) { _logger.LogError(ex, "Ошибка во время вызова сервиса суммаризации."); }
|
||||
catch (OperationCanceledException) { _logger.LogInformation("1.5 Операция суммаризации отменена."); }
|
||||
catch (Exception ex) { _logger.LogError(ex, "1.5 Ошибка во время вызова сервиса суммаризации."); }
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Нет текстов новостей для суммаризации.");
|
||||
_logger.LogInformation("1.5 Нет текстов новостей для суммаризации из канала '{Channel}'.", currentChannelUsername);
|
||||
}
|
||||
|
||||
// --- 2. Формирование текста ---
|
||||
_logger.LogInformation("Формирование текста дайджеста...");
|
||||
_logger.LogInformation("2. Формирование текста дайджеста...");
|
||||
// Можно передать currentChannelUsername для добавления в заголовок дайджеста
|
||||
string digestText = digestBuilder.BuildDigestText(currencyData, cryptoData, weatherData, newsSummary);
|
||||
_logger.LogDebug("Сформированный текст получен от DigestBuilder.");
|
||||
_logger.LogDebug("2. Сформированный текст получен от DigestBuilder.");
|
||||
|
||||
// --- 3. Отправка ---
|
||||
if (!string.IsNullOrWhiteSpace(digestText))
|
||||
{
|
||||
_logger.LogInformation("Отправка дайджеста...");
|
||||
_logger.LogInformation("3. Отправка дайджеста...");
|
||||
bool sent = await telegramSender.SendMessageAsync(digestText, ParseMode.Markdown, stoppingToken);
|
||||
if (!sent) { _logger.LogWarning("Не удалось инициировать отправку дайджеста."); }
|
||||
if (!sent) { _logger.LogWarning("3. Не удалось инициировать отправку дайджеста."); }
|
||||
}
|
||||
else { _logger.LogWarning("Текст дайджеста пуст, отправка не производится."); }
|
||||
else { _logger.LogWarning("3. Текст дайджеста пуст, отправка не производится."); }
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Запрос на остановку получен во время выполнения основной задачи.");
|
||||
break;
|
||||
break; // Выходим из while
|
||||
}
|
||||
catch (Exception ex)
|
||||
{ // Ловим КРИТИЧЕСКИЕ ошибки цикла задачи
|
||||
_logger.LogError(ex, "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста.");
|
||||
try { await telegramSender.SendErrorMessageAsync($"Критическая ошибка в Worker: {ex.Message}", CancellationToken.None); }
|
||||
_logger.LogError(ex, "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста для канала {Channel}.", currentChannelUsername);
|
||||
try { await telegramSender.SendErrorMessageAsync($"Критическая ошибка в Worker для канала {currentChannelUsername}: {ex.Message}", CancellationToken.None); }
|
||||
catch (Exception sendEx) { _logger.LogError(sendEx, "Не удалось отправить сообщение о КРИТИЧЕСКОЙ ошибке."); }
|
||||
// Не делаем Delay, чтобы быстрее перейти к следующему плановому запуску
|
||||
}
|
||||
} // Конец scope
|
||||
_logger.LogInformation("--- Завершение цикла обработки дайджеста для канала: [{ChannelUsername}] ---", currentChannelUsername);
|
||||
|
||||
_logger.LogInformation("Процесс создания дайджеста завершен. Расчет следующего запуска.");
|
||||
|
||||
// Короткая пауза после выполнения, чтобы не запустить случайно дважды в одну секунду
|
||||
// Короткая пауза после выполнения
|
||||
try { await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
|
||||
@ -216,48 +227,51 @@ namespace DailyDigestWorker
|
||||
_logger.LogInformation("Worker останавливается.");
|
||||
}
|
||||
|
||||
// Метод для расчета задержки до ближайшего времени запуска в целевом часовом поясе
|
||||
private TimeSpan CalculateDelayUntilNearestRunInTargetZone(List<TimeSpan> runTimesLocal, TimeZoneInfo targetZone)
|
||||
// Метод расчета задержки И получения СЛЕДУЮЩЕГО задания из расписания
|
||||
private (TimeSpan Delay, string ChannelUsername, DateTime NextRunTargetZone) CalculateDelayAndNextSchedule(
|
||||
List<(TimeSpan TimeLocal, string ChannelUsername)> schedules, TimeZoneInfo targetZone)
|
||||
{
|
||||
// Получаем ТЕКУЩЕЕ время в ЦЕЛЕВОМ часовом поясе
|
||||
// Получаем текущее время в целевом часовом поясе
|
||||
DateTime targetZoneNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, targetZone);
|
||||
DateTime targetZoneToday = targetZoneNow.Date;
|
||||
TimeSpan targetZoneCurrentTimeOfDay = targetZoneNow.TimeOfDay;
|
||||
|
||||
_logger.LogDebug("Текущее время в целевой зоне ({TargetZoneId}): {TargetNow:yyyy-MM-dd HH:mm:ss}", targetZone.Id, targetZoneNow);
|
||||
_logger.LogDebug("Расчет след. запуска: Текущее время в зоне {TargetZoneId}: {TargetNow:yyyy-MM-dd HH:mm:ss.fff}", targetZone.Id, targetZoneNow);
|
||||
|
||||
// Ищем ближайшее время запуска СЕГОДНЯ (в целевой зоне), которое >= текущего времени
|
||||
TimeSpan? nextRunTimeToday = runTimesLocal
|
||||
.Where(t => t >= targetZoneCurrentTimeOfDay)
|
||||
.Cast<TimeSpan?>()
|
||||
.FirstOrDefault(); // Используем FirstOrDefault, т.к. список отсортирован
|
||||
// Ищем первое расписание на СЕГОДНЯ (в целевой зоне), время которого >= текущего времени
|
||||
var nextScheduleToday = schedules
|
||||
.Where(s => s.TimeLocal >= targetZoneCurrentTimeOfDay)
|
||||
.Cast<(TimeSpan TimeLocal, string ChannelUsername)?>()
|
||||
.FirstOrDefault();
|
||||
|
||||
(TimeSpan TimeLocal, string ChannelUsername) targetSchedule;
|
||||
DateTime nextRunTargetZoneDateTime; // Время следующего запуска в целевой зоне
|
||||
|
||||
if (nextRunTimeToday.HasValue)
|
||||
if (nextScheduleToday.HasValue)
|
||||
{
|
||||
// Планируем на сегодня (в целевой зоне)
|
||||
nextRunTargetZoneDateTime = targetZoneToday + nextRunTimeToday.Value;
|
||||
_logger.LogDebug("Ближайшее время запуска сегодня (в целевой зоне): {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunTargetZoneDateTime);
|
||||
// Нашли запуск на сегодня
|
||||
targetSchedule = nextScheduleToday.Value;
|
||||
nextRunTargetZoneDateTime = targetZoneToday + targetSchedule.TimeLocal;
|
||||
_logger.LogDebug("Расчет след. запуска: Найден запуск на сегодня ({Channel}) в {RunTime:HH:mm:ss}", targetSchedule.ChannelUsername, nextRunTargetZoneDateTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Планируем на ЗАВТРА (в целевой зоне), используя самое раннее время
|
||||
var earliestTime = runTimesLocal.First(); // Гарантированно есть и отсортировано
|
||||
nextRunTargetZoneDateTime = targetZoneToday.AddDays(1) + earliestTime;
|
||||
_logger.LogDebug("Все времена запуска сегодня (в целевой зоне) прошли. Ближайшее время завтра: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunTargetZoneDateTime);
|
||||
// Все запуски сегодня прошли, берем первый запуск из списка на ЗАВТРА
|
||||
targetSchedule = schedules.First(); // Гарантированно есть и отсортировано
|
||||
nextRunTargetZoneDateTime = targetZoneToday.AddDays(1) + targetSchedule.TimeLocal;
|
||||
_logger.LogDebug("Расчет след. запуска: Запуски на сегодня прошли. Следующий завтра ({Channel}) в {RunTime:HH:mm:ss}", targetSchedule.ChannelUsername, nextRunTargetZoneDateTime);
|
||||
}
|
||||
|
||||
// Переводим рассчитанное время следующего запуска из целевой зоны обратно в UTC
|
||||
// Переводим время следующего запуска из целевой зоны в UTC
|
||||
DateTime nextRunUtc = TimeZoneInfo.ConvertTimeToUtc(nextRunTargetZoneDateTime, targetZone);
|
||||
|
||||
// Рассчитываем задержку от ТЕКУЩЕГО времени UTC
|
||||
TimeSpan delay = nextRunUtc - DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug("Рассчитанное время следующего запуска UTC: {NextRunUtc:yyyy-MM-dd HH:mm:ss}Z", nextRunUtc);
|
||||
_logger.LogDebug("Расчет след. запуска: Время UTC {NextRunUtc:yyyy-MM-dd HH:mm:ss}Z. Задержка: {Delay}", nextRunUtc, delay);
|
||||
|
||||
// Если задержка отрицательная (уже опоздали), возвращаем Zero для немедленного запуска.
|
||||
return delay < TimeSpan.Zero ? TimeSpan.Zero : delay;
|
||||
// Возвращаем задержку (не меньше нуля), имя канала для этого запуска и время запуска в целевой зоне
|
||||
return (delay < TimeSpan.Zero ? TimeSpan.Zero : delay, targetSchedule.ChannelUsername, nextRunTargetZoneDateTime);
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
|
@ -27,15 +27,34 @@
|
||||
// Для Gemini API URL может быть не нужен, если используется клиентская библиотека Google
|
||||
},
|
||||
"Scheduling": {
|
||||
"RunAtTimesLocal": [
|
||||
"09:00:00",
|
||||
"12:00:00",
|
||||
"15:00:00",
|
||||
"18:00:00",
|
||||
"21:00:00",
|
||||
"00:00:00"
|
||||
],
|
||||
"TargetTimeZoneId": "Asia/Novokuznetsk" // Укажите ваш IANA TimeZone ID
|
||||
"TargetTimeZoneId": "Asia/Novokuznetsk", // Ваш часовой пояс
|
||||
"Schedules": [
|
||||
{
|
||||
"TimeLocal": "09:00:00",
|
||||
"ChannelUsername": "topor"
|
||||
}, // Пример
|
||||
{
|
||||
"TimeLocal": "12:00:00",
|
||||
"ChannelUsername": "exploitex"
|
||||
}, // Пример
|
||||
{
|
||||
"TimeLocal": "15:00:00",
|
||||
"ChannelUsername": "lentachold"
|
||||
}, // Пример
|
||||
{
|
||||
"TimeLocal": "18:00:00",
|
||||
"ChannelUsername": "breakingmash"
|
||||
}, // Пример
|
||||
{
|
||||
"TimeLocal": "21:00:00",
|
||||
"ChannelUsername": "meduzalive"
|
||||
}, // Пример
|
||||
{
|
||||
"TimeLocal": "23:00:00",
|
||||
"ChannelUsername": "readovkanews"
|
||||
} // Пример
|
||||
// Добавьте/измените время и каналы по вашему усмотрению
|
||||
]
|
||||
},
|
||||
"GeminiSettings": {
|
||||
"ModelName": "gemini-1.5-flash-latest", // <--- ИЗМЕНИТЬ ЗДЕСЬ
|
||||
|
Loading…
x
Reference in New Issue
Block a user