Telegram: Bot=Session statt User-Level, Chat-Code-Refactoring

- Telegram-Config (Bot-Token, Webhook-Secret, ChatId) von User-Settings
  in sessions-Tabelle verschoben → jede Session hat ihren eigenen Bot
- Link-Code-System komplett entfernt (TelegramLinkRecord gelöscht)
- /start verknüpft automatisch, kein /link mehr nötig
- Alle Endpoints session-basiert: /api/sessions/{id}/telegram/*
- Webhook-URL pro Session: /api/telegram/webhook/{sessionId}

Refactoring: Doppelten Chat-Code eliminiert
- Neuer ChatService: PrepareContext, BuildHistoryFromDb, ResolveModelId
- Neues ChatContext-Model als gemeinsame Datenstruktur
- AnthropicService.ChatNonStreaming() für Telegram (statt eigenem Loop)
- TelegramService um ~180 Zeilen reduziert, kein eigener Claude-Loop mehr
- ChatEndpoint nutzt ChatService.PrepareContext() statt eigener Setup-Logik
- Model-Mapping nur noch an einer Stelle (3x → 1x)

Frontend
- Telegram-Section von Settings-Page in Session-Bearbeitungsseite verschoben
- Webhook-Secret readonly, wird einmalig generiert und persistent gespeichert
- Status wird automatisch geladen, Register/Delete je nach Webhook-Status
- Toast-Notification-System (showToast) in ui.js
- Link-Code-Modal und Telegram-Menüpunkt im Dashboard entfernt
- Telegram-Badge auf Session-Cards (konfiguriert/verbunden)

Bugfix: Bearer statt x-api-key für OAuth-Tokens im TelegramService
This commit is contained in:
Meleki committed 2026-03-03 21:46:59 +01:00
1 parent a0752ea492
commit 0120bb8f73
21 files changed
+994 -1272

No files matched your search

+11 -87
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
@@ -13,7 +12,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Gott.Endpoints;
@@ -24,7 +22,7 @@ public static class ChatEndpoint
app.MapPost("/api/chat", HandleChat);
}
private static async Task HandleChat(HttpRequest req, HttpResponse res, IHttpClientFactory httpFactory, IConfiguration config, ToolService toolService, DatabaseService db, StreamingTracker streamingTracker, MemoryService memoryService, SkillService skillService, CancellationToken cancellationToken)
private static async Task HandleChat(HttpRequest req, HttpResponse res, IHttpClientFactory httpFactory, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, MemoryService memoryService, ToolService toolService, CancellationToken cancellationToken)
{
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
if (body == null || body.Messages == null || body.Messages.Count == 0)
@@ -50,91 +48,27 @@ public static class ChatEndpoint
return;
}
// Agent aus Session auflösen
var userId = req.HttpContext.GetUserId();
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId || string.IsNullOrEmpty(session.AgentId))
var maxIterations = config.GetValue<int>("MaxIterations", 100);
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations);
if (ctx == null)
{
res.StatusCode = 400;
await res.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
await res.WriteAsJsonAsync(new { error = "Session/Agent nicht gefunden oder kein Agent ausgewählt" });
return;
}
var agent = await db.GetAgent(session.AgentId);
if (agent == null)
{
res.StatusCode = 400;
await res.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
return;
}
var modelId = agent.Model switch
{
"sonnet" => "claude-sonnet-4-6",
"sonnet-4-5" => "claude-sonnet-4-5",
"sonnet-4-6" => "claude-sonnet-4-6",
"opus" => "claude-opus-4-6",
"opus-4-5" => "claude-opus-4-5",
"opus-4-6" => "claude-opus-4-6",
"haiku" => "claude-haiku-4-5",
"haiku-4-5" => "claude-haiku-4-5",
_ => "claude-sonnet-4-6"
};
res.Headers["Content-Type"] = "text/event-stream";
res.Headers["Cache-Control"] = "no-cache";
res.Headers["Connection"] = "keep-alive";
var markedMessages = await db.GetMarkedMessages(sessionId);
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
var agentSkills = await db.GetAgentSkills(agent.AgentId);
var user = await db.GetUser(userId);
var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona());
var chainId = Guid.NewGuid().ToString("N");
var workspace = session.Workspace;
// Skills brauchen Tool-Use (execute) – wenn Skills aktiv aber kein Workspace gesetzt,
// /tmp als Fallback-Workspace setzen damit die Tool-Definitionen generiert werden.
if (string.IsNullOrEmpty(workspace) && agentSkills.Any())
workspace = "/tmp";
var toolChoice = agent.ToolChoice ?? "any";
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId);
var toolDefs = (hasWorkspace || includeMemory)
? toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory)
: Array.Empty<object>();
var toolNames = toolDefs.Length > 0
? JsonSerializer.Serialize(toolDefs.Select(td =>
{
var json = JsonSerializer.Serialize(td);
var el = JsonSerializer.Deserialize<JsonElement>(json);
return el.GetProperty("name").GetString();
}).ToArray())
: null;
await db.CreateChain(new ChainRecord
{
ChainId = chainId,
SessionId = sessionId,
AgentId = agent.AgentId,
Model = modelId,
SystemPrompt = systemPromptText,
ToolChoice = (hasWorkspace || includeMemory) ? toolChoice : null,
ToolNames = toolNames
});
// Session-Timestamp aktualisieren
await db.UpdateSessionTimestamp(sessionId);
// Messages aus dem Request aufbereiten
var messages = new List<object>();
foreach (var msg in body.Messages)
{
// Content bereinigen: interne Felder (_file_name) entfernen
JsonElement content = msg.Content;
if (msg.Content.ValueKind == System.Text.Json.JsonValueKind.Array)
if (msg.Content.ValueKind == JsonValueKind.Array)
{
var node = System.Text.Json.Nodes.JsonNode.Parse(msg.Content.GetRawText());
foreach (var block in node.AsArray())
@@ -157,7 +91,6 @@ public static class ChatEndpoint
if (lastUserMsg != null)
{
var textContent = lastUserMsg.GetTextContent();
var attachmentInfos = lastUserMsg.GetAttachments();
var attachmentBlocks = lastUserMsg.GetAttachmentBlocks();
var attachmentRecords = attachmentBlocks.Select(b => new AttachmentRecord
@@ -170,7 +103,7 @@ public static class ChatEndpoint
await db.SaveMessageWithAttachments(new MessageRecord
{
ChainId = chainId,
ChainId = ctx.ChainId,
Iteration = 0,
Role = "user",
Type = "text",
@@ -178,20 +111,11 @@ public static class ChatEndpoint
}, attachmentRecords);
}
var systemPrompt = string.IsNullOrWhiteSpace(systemPromptText)
? new List<object>()
: new List<object> { new { type = "text", text = systemPromptText } };
if (!string.IsNullOrEmpty(workspace))
Directory.CreateDirectory(workspace);
var maxIterations = config.GetValue<int>("MaxIterations", 100);
var anthropic = new AnthropicService(httpFactory, token, toolService, db, memoryService);
streamingTracker.Start(sessionId, chainId);
streamingTracker.Start(sessionId, ctx.ChainId);
try
{
await anthropic.StreamChat(res, modelId, systemPrompt, messages, chainId, workspace, sessionId, agentId: agent.AgentId, useMemory: agent.UseMemory, webSearchVersion: agent.WebSearchVersion, maxIterations: maxIterations, toolChoice: toolChoice, cancellationToken: cancellationToken);
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: ctx.ToolChoice, cancellationToken: cancellationToken);
}
finally
{
+72 -66
View File
@@ -13,28 +13,29 @@ public static class TelegramEndpoint
{
public static void MapTelegramEndpoints(this IEndpointRouteBuilder app)
{
// Webhook (von Telegram aufgerufen – keine Cookie-Auth!)
app.MapPost("/api/telegram/webhook", HandleWebhook);
// Webhook pro Session (von Telegram aufgerufen – keine Cookie-Auth!)
app.MapPost("/api/telegram/webhook/{sessionId}", HandleWebhook);
// Link-Code generieren (aus Web-UI, braucht Auth)
app.MapPost("/api/telegram/link", HandleCreateLink);
// Session Telegram-Config speichern
app.MapPut("/api/sessions/{sessionId}/telegram", HandleSaveTelegram);
// Webhook registrieren (Admin)
app.MapPost("/api/telegram/register-webhook", HandleRegisterWebhook);
// Session Telegram-Config abrufen
app.MapGet("/api/sessions/{sessionId}/telegram", HandleGetTelegram);
// Telegram-Status (ob konfiguriert – für Frontend)
app.MapGet("/api/telegram/status", HandleStatus);
// Webhook registrieren (pro Session)
app.MapPost("/api/sessions/{sessionId}/telegram/register-webhook", HandleRegisterWebhook);
// Webhook-Info (Admin)
app.MapGet("/api/telegram/webhook-info", HandleWebhookInfo);
// Webhook-Info (pro Session)
app.MapGet("/api/sessions/{sessionId}/telegram/webhook-info", HandleWebhookInfo);
// Webhook löschen (Admin)
app.MapDelete("/api/telegram/webhook", HandleDeleteWebhook);
// Webhook löschen (pro Session)
app.MapDelete("/api/sessions/{sessionId}/telegram/webhook", HandleDeleteWebhook);
}
private static async Task HandleWebhook(HttpRequest req, HttpResponse res, TelegramService telegram)
private static async Task HandleWebhook(string sessionId, HttpRequest req, HttpResponse res, TelegramService telegram, DatabaseService db)
{
if (!telegram.IsConfigured)
var session = await db.GetSession(sessionId);
if (session == null || string.IsNullOrEmpty(session.TelegramBotToken))
{
res.StatusCode = 404;
return;
@@ -42,7 +43,7 @@ public static class TelegramEndpoint
// Secret-Token prüfen
var secret = req.Headers["X-Telegram-Bot-Api-Secret-Token"].ToString();
if (secret != telegram.WebhookSecret)
if (!string.IsNullOrEmpty(session.TelegramWebhookSecret) && secret != session.TelegramWebhookSecret)
{
res.StatusCode = 403;
return;
@@ -54,8 +55,8 @@ public static class TelegramEndpoint
// Fire-and-forget – Telegram erwartet schnelle 200 OK Antwort
_ = Task.Run(async () =>
{
try { await telegram.HandleUpdate(body); }
catch (Exception ex) { Console.Error.WriteLine($"[Telegram] Fehler: {ex.Message}"); }
try { await telegram.HandleUpdate(body, session); }
catch (Exception ex) { Console.Error.WriteLine($"[Telegram] Fehler für Session {sessionId}: {ex.Message}"); }
});
}
catch
@@ -66,26 +67,9 @@ public static class TelegramEndpoint
res.StatusCode = 200;
}
private static async Task HandleCreateLink(HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram)
private static async Task HandleSaveTelegram(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db)
{
if (!telegram.IsConfigured)
{
res.StatusCode = 400;
await res.WriteAsJsonAsync(new { error = "Telegram Bot nicht konfiguriert" });
return;
}
var userId = req.HttpContext.GetUserId();
var body = await JsonSerializer.DeserializeAsync<JsonElement>(req.Body);
if (!body.TryGetProperty("sessionId", out var sidEl))
{
res.StatusCode = 400;
await res.WriteAsJsonAsync(new { error = "sessionId required" });
return;
}
var sessionId = sidEl.GetString();
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
@@ -94,18 +78,42 @@ public static class TelegramEndpoint
return;
}
var agentId = session.AgentId;
var code = await db.CreateTelegramLinkCode(userId, sessionId, agentId);
var body = await JsonSerializer.DeserializeAsync<JsonElement>(req.Body);
var botToken = body.TryGetProperty("botToken", out var t) ? t.GetString() : null;
var webhookSecret = body.TryGetProperty("webhookSecret", out var s) ? s.GetString() : null;
await res.WriteAsJsonAsync(new { code, expiresInSeconds = 300 });
await db.UpdateSessionTelegram(sessionId, botToken, webhookSecret, session.TelegramChatId);
await res.WriteAsJsonAsync(new { ok = true });
}
private static async Task HandleRegisterWebhook(HttpRequest req, HttpResponse res, TelegramService telegram)
private static async Task HandleGetTelegram(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db)
{
if (!req.HttpContext.IsAdmin())
var userId = req.HttpContext.GetUserId();
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
res.StatusCode = 403;
await res.WriteAsJsonAsync(new { error = "Admin required" });
res.StatusCode = 404;
await res.WriteAsJsonAsync(new { error = "Session nicht gefunden" });
return;
}
await res.WriteAsJsonAsync(new
{
botToken = session.TelegramBotToken ?? "",
webhookSecret = session.TelegramWebhookSecret ?? "",
chatId = session.TelegramChatId ?? "",
configured = !string.IsNullOrEmpty(session.TelegramBotToken)
});
}
private static async Task HandleRegisterWebhook(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram)
{
var userId = req.HttpContext.GetUserId();
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
res.StatusCode = 404;
await res.WriteAsJsonAsync(new { error = "Session nicht gefunden" });
return;
}
@@ -117,39 +125,37 @@ public static class TelegramEndpoint
return;
}
var result = await telegram.RegisterWebhook(urlEl.GetString());
await res.WriteAsJsonAsync(new { result });
}
private static async Task HandleStatus(HttpResponse res, TelegramService telegram)
{
await res.WriteAsJsonAsync(new { configured = telegram.IsConfigured });
}
private static async Task HandleWebhookInfo(HttpRequest req, HttpResponse res, TelegramService telegram)
{
if (!req.HttpContext.IsAdmin())
{
res.StatusCode = 403;
await res.WriteAsJsonAsync(new { error = "Admin required" });
return;
}
var result = await telegram.GetWebhookInfo();
var result = await telegram.RegisterWebhook(urlEl.GetString(), sessionId);
res.ContentType = "application/json";
await res.WriteAsync(result);
}
private static async Task HandleDeleteWebhook(HttpRequest req, HttpResponse res, TelegramService telegram)
private static async Task HandleWebhookInfo(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram)
{
if (!req.HttpContext.IsAdmin())
var userId = req.HttpContext.GetUserId();
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
res.StatusCode = 403;
await res.WriteAsJsonAsync(new { error = "Admin required" });
res.StatusCode = 404;
return;
}
var result = await telegram.DeleteWebhook();
var result = await telegram.GetWebhookInfo(sessionId);
res.ContentType = "application/json";
await res.WriteAsync(result);
}
private static async Task HandleDeleteWebhook(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram)
{
var userId = req.HttpContext.GetUserId();
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
res.StatusCode = 404;
return;
}
var result = await telegram.DeleteWebhook(sessionId);
res.ContentType = "application/json";
await res.WriteAsync(result);
}