feat(telegram): CLI-Fallback bei fehlendem API-Key (analog Web-UI)

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) <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.7 committed 2026-05-31 20:44:36 +02:00
1 parent 205dbab6a0
commit 271902f32d
2 files changed
+208 -3

No files matched your search

+170
View File
@@ -366,6 +366,176 @@ public partial class AnthropicService
}
}
/// <summary>
/// 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.
/// </summary>
public async Task<string> ChatNonStreamingViaCli(
ChatContext ctx,
List<object> messages,
Func<int, Task> 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<string>();
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 = $"<conversation_history>\n{historyContext}\n</conversation_history>\n\n<current_request>\n{prompt}\n</current_request>";
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);
}
}
/// <summary>
/// Mappt CLI-Tool-Namen (Edit, Write, Bash, etc.) auf gott-Tool-Namen (edit_file, write_file, etc.).
/// </summary>
+38 -3
View File
@@ -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)