фикс с разными тгк

This commit is contained in:
Professional 2025-04-12 10:20:46 +07:00
parent b861e99ce2
commit 2553b13367
6 changed files with 262 additions and 170 deletions

View 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; }
}
}

View File

@ -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"; // Безопасное значение по умолчанию
}
}

View File

@ -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);
}
}

View File

@ -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
View File

@ -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)

View File

@ -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", // <--- ИЗМЕНИТЬ ЗДЕСЬ