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:
1 parent
205dbab6a0
commit
271902f32d
2 files changed
+208
-3
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user