From e3f89043c108ba330b8a1c6acf6ea2867049cdb0 Mon Sep 17 00:00:00 2001 From: Meleki Date: Fri, 19 Jun 2026 13:25:05 +0200 Subject: [PATCH] Fix cronjob/telegram fallback for keyless server-key providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cronjobs and Telegram bots threw a hard error when an agent had no assigned user API key, even for providers (nvidia/gemini/minimax/ pollinations) whose server-side key ChatEndpoint resolves automatically via ResolveProviderConfig. The guard blocked every non-empty provider instead of only "openai", so e.g. the nvidia agent failed on "Jetzt ausführen" although the normal chat worked. Mirror ChatEndpoint's three-way handling at all three sites (ExecuteCronjob, SendCronjobNotification, TelegramService.HandleMessage): - Anthropic (empty provider): Claude CLI fallback when permitted/available - "openai": hard error (no server key, no CLI equivalent) - other providers: pass through with empty token; server key resolved later Co-Authored-By: Claude Opus 4.8 --- Services/CronjobService.cs | 66 ++++++++++++++++++++++++++++++++++--- Services/TelegramService.cs | 43 +++++++++++++----------- 2 files changed, 85 insertions(+), 24 deletions(-) diff --git a/Services/CronjobService.cs b/Services/CronjobService.cs index 1e51321..6b1cf86 100644 --- a/Services/CronjobService.cs +++ b/Services/CronjobService.cs @@ -91,9 +91,37 @@ public class CronjobService : BackgroundService // 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) - throw new Exception($"Kein API-Key für Agent {ctx.AgentId} zugewiesen (User {job.UserId})"); - ctx.ApiToken = userApiKey.ApiKey; + { + // 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. @@ -117,6 +145,8 @@ public class CronjobService : BackgroundService 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); @@ -150,12 +180,36 @@ public class CronjobService : BackgroundService // 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) { - _logger.LogWarning("Kein API-Key für Agent {AgentId} (Cronjob '{Name}' Notification)", notifyCtx.AgentId, job.Name); - return; + // 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; } - 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)}"; @@ -183,6 +237,8 @@ public class CronjobService : BackgroundService 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); diff --git a/Services/TelegramService.cs b/Services/TelegramService.cs index 8cc28a2..ebc00f0 100644 --- a/Services/TelegramService.cs +++ b/Services/TelegramService.cs @@ -177,28 +177,33 @@ public class TelegramService if (userApiKey == null) { - // Kein Key → CLI-Fallback prüfen (analog ChatEndpoint). - // Nur für Anthropic-Agents möglich (CLI = Claude). OpenAI-Provider haben kein CLI-Äquivalent. - if (!string.IsNullOrEmpty(ctx.Provider)) + // Kein User-Key – Verhalten exakt wie 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(ctx.Provider)) { - await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen. Für OpenAI-kompatible Provider ist kein CLI-Fallback möglich – der Bot-Besitzer muss im Web-UI einen Key zuweisen."); + var botOwner = await _db.GetUser(botUserId); + if (botOwner != null && !botOwner.CanUseCli && !botOwner.IsAdmin) + { + await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen und der Bot-Besitzer hat keine CLI-Berechtigung."); + return; + } + + if (!_anthropic.IsCliAvailable) + { + await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen und Claude CLI nicht verfügbar. Der Bot-Besitzer muss im Web-UI einen Key zuweisen oder `claude login` auf dem Server ausführen."); + return; + } + + useCliFallback = true; + } + else if (ctx.Provider == "openai") + { + await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen. OpenAI benötigt zwingend einen Key – der Bot-Besitzer muss im Web-UI einen Key zuweisen."); return; } - - var botOwner = await _db.GetUser(botUserId); - if (botOwner != null && !botOwner.CanUseCli && !botOwner.IsAdmin) - { - await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen und der Bot-Besitzer hat keine CLI-Berechtigung."); - return; - } - - if (!_anthropic.IsCliAvailable) - { - await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen und Claude CLI nicht verfügbar. Der Bot-Besitzer muss im Web-UI einen Key zuweisen oder `claude login` auf dem Server ausführen."); - return; - } - - useCliFallback = true; + // sonst: nvidia/gemini/minimax/pollinations → ctx.ApiToken bleibt leer (Server-Key-Auflösung). } else {