From 271902f32d16f8717eb2611fc79e1ac289313f09 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sun, 31 May 2026 20:44:36 +0200 Subject: [PATCH] feat(telegram): CLI-Fallback bei fehlendem API-Key (analog Web-UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wenn ein Telegram-Bot eine Nachricht empfängt und für den Agent kein API-Key zugewiesen ist, fiel der Bot bisher hart aus ("Kein API-Key für diesen Agent zugewiesen"). Im Web-UI wird in dem Fall bereits auf Claude-CLI zurückgefallen – das macht der Telegram-Pfad jetzt analog. - AnthropicService.Cli: Neue Methode ChatNonStreamingViaCli(...) für Non-SSE-Caller (Telegram). Macht dasselbe wie StreamChatViaCli aber ohne SSE: System-Prompt + Auto-Context, History-Kontext-Block, letzte User-Message (inkl. Image/PDF-Refs), Text + tool_use + tool_result in DB persistieren, finalen Text zurückgeben + FinalizeChain mit echten Token-Counts vom CLI-Result. - TelegramService: Bei userApiKey == null jetzt CanUseCli, IsAdmin und IsCliAvailable prüfen. Bei OpenAI-Providern bleibt der Hard-Fail (kein CLI-Äquivalent). Spezifische Fehlermeldungen pro Szenario. Zusätzlich Schutz gegen leere Antwort. Co-Authored-By: Claude Opus 4.7 (1M context) --- Services/AnthropicService.Cli.cs | 170 +++++++++++++++++++++++++++++++ Services/TelegramService.cs | 41 +++++++- 2 files changed, 208 insertions(+), 3 deletions(-) diff --git a/Services/AnthropicService.Cli.cs b/Services/AnthropicService.Cli.cs index fd8a99a..d16e9fa 100644 --- a/Services/AnthropicService.Cli.cs +++ b/Services/AnthropicService.Cli.cs @@ -366,6 +366,176 @@ public partial class AnthropicService } } + /// + /// Non-Streaming Chat über die Claude CLI für Telegram (und andere Caller, die kein SSE brauchen). + /// Sammelt den finalen Text und gibt ihn zurück. Persistiert Text, Tool-Use und Tool-Result in der DB, + /// damit die History im Web-UI korrekt rekonstruiert werden kann. + /// + public async Task ChatNonStreamingViaCli( + ChatContext ctx, + List messages, + Func onIteration = null, + CancellationToken cancellationToken = default) + { + if (_cliService == null || !_cliService.IsAvailable) + return ""; + + var sw = System.Diagnostics.Stopwatch.StartNew(); + var usage = new TokenUsage(); + var tempImagePaths = new List(); + + var chain = new ChainRecord + { + ChainId = ctx.ChainId, + SessionId = ctx.SessionId, + Model = ctx.ModelId + }; + + try + { + // System-Prompt als Text + var systemText = new StringBuilder(ctx.SystemPromptText ?? ""); + + // Auto-Context (analog Streaming-Pfad) + if ((ctx.Agent?.UseAutoContext ?? true) && messages.Count > 0) + { + try + { + var autoCtx = await BuildAutoContextAsync(_vectorSearch, messages, ctx.ChainId, ctx.AgentId); + if (!string.IsNullOrEmpty(autoCtx)) + { + systemText.AppendLine(autoCtx); + await _db.SaveMessage(new MessageRecord + { + ChainId = ctx.ChainId, + Role = "system", + Type = "auto_context", + Content = autoCtx + }); + } + } + catch { /* silent – Auto-Context optional */ } + } + + // Prompt aus letzter User-Message + Bild-/PDF-Refs + History-Kontext + var prompt = ExtractLastUserText(messages) ?? ""; + var imageRefs = ExtractLastUserImageRefs(messages, tempImagePaths); + if (!string.IsNullOrEmpty(imageRefs)) + prompt = imageRefs + "\n" + prompt; + + var historyContext = BuildHistoryContext(messages); + if (!string.IsNullOrEmpty(historyContext)) + prompt = $"\n{historyContext}\n\n\n\n{prompt}\n"; + + var fullText = new StringBuilder(); + if (onIteration != null) await onIteration(1); + + var result = await _cliService.ChatStreamingAsync( + prompt, + async evt => + { + switch (evt.Type) + { + case CliEventType.TextDelta: + fullText.Append(evt.Text); + break; + case CliEventType.ToolStart: + // Text-Puffer flushen bevor Tool-Use gespeichert wird (korrekte Reihenfolge in DB) + if (fullText.Length > 0) + { + var textForEmbed = fullText.ToString(); + var preMsgId = await _db.SaveMessage(new MessageRecord + { + ChainId = ctx.ChainId, + Iteration = 1, + Role = "assistant", + Type = "text", + Content = textForEmbed + }); + _embeddingBg?.Enqueue(preMsgId, ctx.ChainId, ctx.SessionId, ctx.AgentId, "assistant", textForEmbed); + fullText.Clear(); + } + if (onIteration != null) await onIteration(1); + break; + case CliEventType.ToolEnd: + { + var mappedName = MapCliToolName(evt.ToolName); + var mappedInput = MapCliToolInput(evt.ToolName, evt.ToolInput); + await _db.SaveMessage(new MessageRecord + { + ChainId = ctx.ChainId, + Iteration = 1, + Role = "assistant", + Type = "tool_use", + ToolName = mappedName, + ToolCallId = evt.ToolId, + Content = mappedInput + }); + break; + } + case CliEventType.ToolResult: + await _db.SaveMessage(new MessageRecord + { + ChainId = ctx.ChainId, + Iteration = 1, + Role = "user", + Type = "tool_result", + ToolCallId = evt.ToolId, + Content = evt.ToolResultContent ?? "", + IsError = evt.ToolResultIsError + }); + break; + } + }, + modelId: ctx.ModelId, + systemPrompt: systemText.ToString(), + workspace: ctx.Workspace, + hasWorkspace: !string.IsNullOrEmpty(ctx.Workspace), + cancellationToken: cancellationToken + ); + + // Finalen Text persistieren + var finalText = fullText.ToString(); + if (finalText.Length > 0) + { + var msgId = await _db.SaveMessage(new MessageRecord + { + ChainId = ctx.ChainId, + Iteration = 1, + Role = "assistant", + Type = "text", + Content = finalText + }); + _embeddingBg?.Enqueue(msgId, ctx.ChainId, ctx.SessionId, ctx.AgentId, "assistant", finalText); + } + + usage.InputTokens = result.InputTokens; + usage.OutputTokens = result.OutputTokens; + usage.CacheReadTokens = result.CacheReadTokens; + usage.CacheCreationTokens = result.CacheCreationTokens; + + var stopReason = result.Success ? "end_turn" : "error"; + await FinalizeChain(chain, usage, result.NumTurns > 0 ? result.NumTurns : 1, stopReason, sw); + + return finalText; + } + catch (Exception ex) + { + await _db.SaveMessage(new MessageRecord + { + ChainId = ctx.ChainId, Iteration = 1, + Role = "system", Type = "error", + Content = $"CLI-Fehler: {ex.Message}", IsError = true + }); + await FinalizeChain(chain, usage, 1, "error", sw); + return ""; + } + finally + { + CleanupTempImages(tempImagePaths); + } + } + /// /// Mappt CLI-Tool-Namen (Edit, Write, Bash, etc.) auf gott-Tool-Namen (edit_file, write_file, etc.). /// diff --git a/Services/TelegramService.cs b/Services/TelegramService.cs index 683d4d2..8cc28a2 100644 --- a/Services/TelegramService.cs +++ b/Services/TelegramService.cs @@ -173,12 +173,37 @@ public class TelegramService // API-Key des Bot-Owners für diesen Agent auflösen var userApiKey = await _db.GetAgentApiKeyRecord(botUserId, ctx.AgentId); + bool useCliFallback = false; + if (userApiKey == null) { - await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen. Der Bot-Besitzer muss im Web-UI einen Key zuweisen."); - return; + // 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)) + { + 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."); + 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; + } + else + { + ctx.ApiToken = userApiKey.ApiKey; } - ctx.ApiToken = userApiKey.ApiKey; await SendChatAction(chatId, botToken, "typing"); @@ -211,6 +236,13 @@ public class TelegramService await SendChatAction(chatId, botToken, "typing"); }); } + else if (useCliFallback) + { + reply = await _anthropic.ChatNonStreamingViaCli(ctx, history, onIteration: async (iteration) => + { + await SendChatAction(chatId, botToken, "typing"); + }); + } else { reply = await _anthropic.ChatNonStreaming(ctx, history, onIteration: async (iteration) => @@ -220,6 +252,9 @@ public class TelegramService }); } + if (string.IsNullOrEmpty(reply)) + reply = "⚠️ Keine Antwort vom Modell erhalten."; + await SendLongMessage(chatId, botToken, reply); } catch (Exception ex)