using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Cronos; using Gott.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Gott.Services; public class CronjobService : BackgroundService { private readonly DatabaseService _db; private readonly ChatService _chatService; private readonly AnthropicService _anthropicService; private readonly TelegramService _telegramService; private readonly ILogger _logger; public CronjobService( DatabaseService db, ChatService chatService, AnthropicService anthropicService, TelegramService telegramService, ILogger logger) { _db = db; _chatService = chatService; _anthropicService = anthropicService; _telegramService = telegramService; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("CronjobService gestartet"); while (!stoppingToken.IsCancellationRequested) { try { await ProcessDueCronjobs(); } catch (Exception ex) { _logger.LogError(ex, "Fehler beim Verarbeiten der Cronjobs"); } // Jede Minute prüfen await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } } private async Task ProcessDueCronjobs() { var dueJobs = await _db.GetDueCronjobs(); foreach (var job in dueJobs) { _logger.LogInformation("Cronjob '{Name}' ({Id}) wird ausgeführt", job.Name, job.CronjobId); try { await ExecuteCronjob(job); var nextRun = CalculateNextRun(job.CronExpression); await _db.UpdateCronjobRun(job.CronjobId, "success", null, nextRun); _logger.LogInformation("Cronjob '{Name}' erfolgreich. Nächste Ausführung: {Next}", job.Name, nextRun); } catch (Exception ex) { _logger.LogError(ex, "Cronjob '{Name}' fehlgeschlagen", job.Name); var nextRun = CalculateNextRun(job.CronExpression); await _db.UpdateCronjobRun(job.CronjobId, "error", ex.Message, nextRun); } } } public async Task ExecuteCronjob(CronjobRecord job) { // Chat-Context vorbereiten (wie bei normalem Chat) var ctx = await _chatService.PrepareContext(job.SessionId, job.UserId); if (ctx == null) throw new Exception("Session oder Agent nicht gefunden / nicht konfiguriert"); // API-Key des Cronjob-Owners auflösen var userApiKey = await _db.GetUserSelectedApiKey(job.UserId); if (userApiKey == null) throw new Exception($"Kein API-Key für User {job.UserId} ausgewählt"); ctx.ApiToken = userApiKey.ApiKey; // History aus DB laden var history = await _chatService.BuildHistoryFromDb(job.SessionId, promptBlocks: ctx.PromptBlocks); // User-Nachricht hinzufügen var userContent = new List { new { type = "text", text = job.Prompt } }; history.Add(new Dictionary { ["role"] = "user", ["content"] = userContent }); // User-Nachricht in DB speichern await _db.SaveMessage(new MessageRecord { ChainId = ctx.ChainId, Iteration = 0, Role = "user", Type = "text", Content = job.Prompt }); // Non-Streaming Chat ausführen var responseText = await _anthropicService.ChatNonStreaming(ctx, history); // Session-Timestamp aktualisieren await _db.UpdateSessionTimestamp(job.SessionId); // Telegram-Benachrichtigung auf der Arbeits-Session senden, falls sie Telegram hat if (!string.IsNullOrEmpty(responseText)) { await _telegramService.SendNotification(job.SessionId, $"⏰ Cronjob: {job.Name}\n📂 Session: {job.SessionName}\n\n{responseText}"); } // Benachrichtigungs-Session: Zusammenfassung in eine andere Session schreiben if (!string.IsNullOrEmpty(job.NotifySessionId) && job.NotifySessionId != job.SessionId) { await SendCronjobNotification(job, responseText); } } private async Task SendCronjobNotification(CronjobRecord job, string responseText) { try { var notifyCtx = await _chatService.PrepareContext(job.NotifySessionId, job.UserId); if (notifyCtx == null) { _logger.LogWarning("Notification-Session {SessionId} für Cronjob '{Name}' nicht gefunden", job.NotifySessionId, job.Name); return; } // API-Key des Cronjob-Owners auflösen var notifyApiKey = await _db.GetUserSelectedApiKey(job.UserId); if (notifyApiKey == null) { _logger.LogWarning("Kein API-Key für User {UserId} (Cronjob '{Name}' Notification)", job.UserId, job.Name); return; } notifyCtx.ApiToken = notifyApiKey.ApiKey; // Kurze Zusammenfassung als User-Nachricht in die Notify-Session schreiben var summary = $"[Cronjob \"{job.Name}\" wurde in Session \"{job.SessionName}\" ausgeführt]\n\nAntwort:\n{Truncate(responseText, 2000)}"; // In Notify-Session als Nachricht speichern await _db.SaveMessage(new MessageRecord { ChainId = notifyCtx.ChainId, Iteration = 0, Role = "user", Type = "text", Content = summary }); // Nur die Benachrichtigung als History senden (nicht volle Session-History, // da diese mit einer assistant-Nachricht enden könnte → prefill-Fehler) var notifyHistory = new List { new Dictionary { ["role"] = "user", ["content"] = new List { new { type = "text", text = summary } } } }; var notifyResponse = await _anthropicService.ChatNonStreaming(notifyCtx, notifyHistory); await _db.UpdateSessionTimestamp(job.NotifySessionId); // Telegram auf Notify-Session senden if (!string.IsNullOrEmpty(notifyResponse)) { await _telegramService.SendNotification(job.NotifySessionId, $"📋 Cronjob: {job.Name}\n📂 Session: {job.SessionName}\n\n{notifyResponse}"); } } catch (Exception ex) { _logger.LogError(ex, "Fehler beim Senden der Cronjob-Benachrichtigung an Session {SessionId}", job.NotifySessionId); } } private static string Truncate(string text, int maxLength) { if (string.IsNullOrEmpty(text) || text.Length <= maxLength) return text ?? ""; return text.Substring(0, maxLength) + "…"; } public static string CalculateNextRun(string cronExpression) { try { var cron = CronExpression.Parse(cronExpression, CronFormat.Standard); // Cron-Expression in Server-Lokalzeit interpretieren var next = cron.GetNextOccurrence(DateTimeOffset.Now, TimeZoneInfo.Local); return next?.UtcDateTime.ToString("o") ?? ""; } catch { return ""; } } }