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:
1 parent
a0752ea492
commit
0120bb8f73
21 files changed
+994
-1272
No files matched your search
+11
-87
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user