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