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 OpenAiService _openAiService; private readonly TelegramService _telegramService; private readonly ILogger _logger; public CronjobService( DatabaseService db, ChatService chatService, AnthropicService anthropicService, OpenAiService openAiService, TelegramService telegramService, ILogger logger) { _db = db; _chatService = chatService; _anthropicService = anthropicService; _openAiService = openAiService; _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 für diesen Agent auflösen var userApiKey = await _db.GetAgentApiKeyRecord(job.UserId, ctx.AgentId); bool useCliFallback = false; if (userApiKey == null) { // Kein User-Key – Verhalten exakt wie ChatEndpoint: // - Anthropic (Provider leer) → Claude-CLI-Fallback (sofern berechtigt/verfügbar) // - "openai" → harter Fehler (kein Server-Key, kein CLI-Äquivalent) // - nvidia/gemini/minimax/pollinations → durchlassen: ResolveProviderConfig löst den Server-Key auf if (string.IsNullOrEmpty(ctx.Provider)) { var jobOwner = await _db.GetUser(job.UserId); if (jobOwner != null && !jobOwner.CanUseCli && !jobOwner.IsAdmin) throw new Exception($"Kein API-Key für Agent {ctx.AgentId} zugewiesen und User {job.UserId} hat keine CLI-Berechtigung."); if (!_anthropicService.IsCliAvailable) throw new Exception($"Kein API-Key für Agent {ctx.AgentId} zugewiesen und Claude CLI nicht verfügbar."); useCliFallback = true; _logger.LogInformation("Cronjob '{Name}': Kein API-Key für Agent {AgentId}, verwende CLI-Fallback", job.Name, ctx.AgentId); } else if (ctx.Provider == "openai") { throw new Exception($"Kein API-Key für Agent {ctx.AgentId} zugewiesen (User {job.UserId}). OpenAI benötigt zwingend einen zugewiesenen API-Key."); } // sonst: nvidia/gemini/minimax/pollinations → ctx.ApiToken bleibt leer, // der Server-seitige Provider-Key wird in ResolveProviderConfig aufgelöst. } else { ctx.ApiToken = userApiKey.ApiKey; } // History aus DB laden. includeThinking=true für OpenAI-kompatible Provider → // ...-Prefix wird dem Assistant-Text vorangestellt. var history = await _chatService.BuildHistoryFromDb(job.SessionId, promptBlocks: ctx.PromptBlocks, includeThinking: !string.IsNullOrEmpty(ctx.Provider)); // 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 – Provider-Weiche string responseText; if (!string.IsNullOrEmpty(ctx.Provider)) responseText = await _openAiService.ChatNonStreaming(ctx, history); else if (useCliFallback) responseText = await _anthropicService.ChatNonStreamingViaCli(ctx, history); else 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 für Notify-Agent auflösen var notifyApiKey = await _db.GetAgentApiKeyRecord(job.UserId, notifyCtx.AgentId); bool notifyUseCliFallback = false; if (notifyApiKey == null) { // Kein User-Key – Verhalten wie ExecuteCronjob/ChatEndpoint: // - Anthropic (Provider leer) → Claude-CLI-Fallback (sofern berechtigt/verfügbar) // - "openai" → abbrechen (kein Server-Key, kein CLI-Äquivalent) // - nvidia/gemini/minimax/pollinations → durchlassen: Server-Key via ResolveProviderConfig if (string.IsNullOrEmpty(notifyCtx.Provider)) { var notifyOwner = await _db.GetUser(job.UserId); if ((notifyOwner != null && !notifyOwner.CanUseCli && !notifyOwner.IsAdmin) || !_anthropicService.IsCliAvailable) { _logger.LogWarning("Kein API-Key für Agent {AgentId} (Cronjob '{Name}' Notification) und CLI nicht verfügbar/berechtigt", notifyCtx.AgentId, job.Name); return; } notifyUseCliFallback = true; } else if (notifyCtx.Provider == "openai") { _logger.LogWarning("Kein API-Key für Agent {AgentId} (Cronjob '{Name}' Notification, OpenAI benötigt zwingend einen Key)", notifyCtx.AgentId, job.Name); return; } // sonst: nvidia/gemini/minimax/pollinations → notifyCtx.ApiToken bleibt leer (Server-Key-Auflösung). } else { 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 } } } }; string notifyResponse; if (!string.IsNullOrEmpty(notifyCtx.Provider)) notifyResponse = await _openAiService.ChatNonStreaming(notifyCtx, notifyHistory); else if (notifyUseCliFallback) notifyResponse = await _anthropicService.ChatNonStreamingViaCli(notifyCtx, notifyHistory); else 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 ""; } } }