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);
}
+1 -1
View File
@@ -18,7 +18,7 @@ public class AuthMiddleware
var path = ctx.Request.Path.Value ?? "";
// Static files, login and telegram webhook don't require auth
var isTelegramWebhook = path == "/api/telegram/webhook" && ctx.Request.Method == "POST";
var isTelegramWebhook = path.StartsWith("/api/telegram/webhook/") && ctx.Request.Method == "POST";
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook)
{
await _next(ctx);
+20
View File
@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace Gott.Models;
public class ChatContext
{
public string ChainId { get; set; }
public string SessionId { get; set; }
public string ModelId { get; set; }
public string Workspace { get; set; }
public string AgentId { get; set; }
public string SystemPromptText { get; set; }
public List<object> SystemPrompt { get; set; }
public string ToolChoice { get; set; }
public bool UseMemory { get; set; }
public string WebSearchVersion { get; set; }
public int MaxIterations { get; set; }
public AgentRecord Agent { get; set; }
public SessionRecord Session { get; set; }
}
+3
View File
@@ -13,6 +13,9 @@ public class SessionRecord
public string CustomCommands { get; set; }
public string UserId { get; set; }
public bool IsDeleted { get; set; }
public string TelegramBotToken { get; set; }
public string TelegramWebhookSecret { get; set; }
public string TelegramChatId { get; set; }
public string CreatedAt { get; set; }
public string UpdatedAt { get; set; }
}
-14
View File
@@ -1,14 +0,0 @@
namespace Gott.Models;
public class TelegramLinkRecord
{
public long Id { get; set; }
public long TelegramChatId { get; set; }
public string UserId { get; set; }
public string SessionId { get; set; }
public string AgentId { get; set; }
public string LinkCode { get; set; }
public string ExpiresAt { get; set; }
public string CreatedAt { get; set; }
public string UpdatedAt { get; set; }
}
+11 -7
View File
@@ -20,7 +20,6 @@ dbService.EnsureDefaultUser(authService);
builder.Services.AddSingleton(authService);
builder.Services.AddSingleton(dbService);
builder.Services.AddSingleton(new ToolService());
builder.Services.AddSingleton(new ImageService());
builder.Services.AddSingleton(new StreamingTracker());
var memoryService = new MemoryService(dataDir);
@@ -28,16 +27,21 @@ var skillService = new SkillService(dataDir);
builder.Services.AddSingleton(memoryService);
builder.Services.AddSingleton(skillService);
// Telegram Bot (optional – nur wenn Token konfiguriert)
var telegramToken = builder.Configuration["TelegramBotToken"] ?? "";
var telegramSecret = builder.Configuration["TelegramWebhookSecret"] ?? Guid.NewGuid().ToString("N");
// Shared Services
var toolService = new ToolService();
builder.Services.AddSingleton(toolService);
var chatService = new ChatService(dbService, toolService, memoryService, skillService);
builder.Services.AddSingleton(chatService);
// Telegram Bot (pro Session – Token aus Session-Config)
var anthropicToken = builder.Configuration["AnthropicToken"] ?? "";
var maxIterations = builder.Configuration.GetValue<int>("MaxIterations", 100);
builder.Services.AddSingleton<TelegramService>(sp =>
new TelegramService(
telegramToken, telegramSecret, anthropicToken,
anthropicToken,
sp.GetRequiredService<IHttpClientFactory>(),
dbService, new ToolService(), memoryService, skillService,
builder.Configuration.GetValue<int>("MaxIterations", 100)));
dbService, toolService, memoryService, chatService,
maxIterations));
var app = builder.Build();
app.UseDefaultFiles();
+179
View File
@@ -235,6 +235,185 @@ public class AnthropicService
}
}
/// <summary>
/// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück.
/// onIteration wird nach jeder Iteration aufgerufen (z.B. für Typing-Indicator).
/// </summary>
public async Task<string> ChatNonStreaming(
ChatContext ctx,
List<object> messages,
Func<int, Task> onIteration = null)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var usage = new TokenUsage();
int iterations = 0;
var finalText = new StringBuilder();
var chain = new ChainRecord
{
ChainId = ctx.ChainId,
SessionId = ctx.SessionId,
Model = ctx.ModelId
};
try
{
while (iterations < ctx.MaxIterations)
{
iterations++;
onIteration?.Invoke(iterations);
var payload = new Dictionary<string, object>
{
["model"] = ctx.ModelId,
["max_tokens"] = 16384,
["messages"] = messages,
["stream"] = false
};
if (ctx.SystemPrompt != null && ctx.SystemPrompt.Count > 0)
payload["system"] = ctx.SystemPrompt;
var hasWorkspace = !string.IsNullOrEmpty(ctx.Workspace);
var includeMemory = ctx.UseMemory && _memoryService != null && !string.IsNullOrEmpty(ctx.AgentId);
var hasTools = hasWorkspace || includeMemory;
var hasWebSearch = !string.IsNullOrEmpty(ctx.WebSearchVersion);
if (hasTools || hasWebSearch)
{
var tools = new List<object>();
if (hasTools)
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory));
if (hasWebSearch)
tools.Add(new { type = ctx.WebSearchVersion, name = "web_search", max_uses = 5 });
payload["tools"] = tools;
var effectiveChoice = (ctx.ToolChoice == "any" && iterations == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
var client = CreateClient();
var json = JsonSerializer.Serialize(payload);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
var httpResp = await client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent });
var respBody = await httpResp.Content.ReadAsStringAsync();
if (!httpResp.IsSuccessStatusCode)
{
var errMsg = $"API {(int)httpResp.StatusCode}: {respBody}";
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "system", Type = "error",
Content = errMsg, IsError = true
});
await FinalizeChain(chain, usage, iterations, "error", sw);
throw new Exception($"Claude API Fehler: {(int)httpResp.StatusCode}");
}
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
// Usage tracken
if (resp.TryGetProperty("usage", out var u))
{
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
}
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
var content = resp.GetProperty("content");
var assistantContent = new List<object>();
var toolResults = new List<object>();
foreach (var block in content.EnumerateArray())
{
var blockType = block.GetProperty("type").GetString() ?? "";
if (blockType == "text")
{
var text = block.GetProperty("text").GetString() ?? "";
assistantContent.Add(new { type = "text", text });
finalText.Append(text);
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "assistant", Type = "text",
Content = text
});
}
else if (blockType == "tool_use")
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
var toolInput = block.GetProperty("input");
assistantContent.Add(new { type = "tool_use", id = toolId, name = toolName, input = toolInput });
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "assistant", Type = "tool_use",
Content = toolInput.GetRawText(),
ToolName = toolName, ToolCallId = toolId
});
string toolResult;
bool isError = false;
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId); }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
// Tool-Result in der History kürzen (wie bei StreamChat)
const int MaxToolResultChars = 20_000;
var truncatedResult = toolResult.Length > MaxToolResultChars
? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]"
: toolResult;
toolResults.Add(new
{
type = "tool_result",
tool_use_id = toolId,
content = truncatedResult,
is_error = isError
});
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "user", Type = "tool_result",
Content = toolResult,
ToolName = toolName, ToolCallId = toolId,
IsError = isError
});
}
}
messages.Add(new Dictionary<string, object> { ["role"] = "assistant", ["content"] = assistantContent });
if (stopReason == "end_turn" || stopReason == "stop_sequence" || toolResults.Count == 0)
{
await FinalizeChain(chain, usage, iterations, stopReason, sw);
break;
}
messages.Add(new Dictionary<string, object> { ["role"] = "user", ["content"] = toolResults });
}
}
catch
{
if (iterations > 0)
await FinalizeChain(chain, usage, iterations, "error", sw);
throw;
}
return finalText.Length > 0 ? finalText.ToString() : "(Keine Textantwort)";
}
private async Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw, string streamErrorInfo = null)
{
chain.InputTokens = usage.InputTokens;
+234
View File
@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Models;
namespace Gott.Services;
public class ChatService
{
private readonly DatabaseService _db;
private readonly ToolService _toolService;
private readonly MemoryService _memoryService;
private readonly SkillService _skillService;
public ChatService(DatabaseService db, ToolService toolService, MemoryService memoryService, SkillService skillService)
{
_db = db;
_toolService = toolService;
_memoryService = memoryService;
_skillService = skillService;
}
public static string ResolveModelId(string model)
{
return 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"
};
}
/// <summary>
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
/// </summary>
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100)
{
var session = await _db.GetSession(sessionId);
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
return null;
var agent = await _db.GetAgent(session.AgentId);
if (agent == null)
return null;
var modelId = ResolveModelId(agent.Model);
var markedMessages = await _db.GetMarkedMessages(sessionId);
var agentMemoryDir = _memoryService.GetAgentMemoryDir(agent.AgentId);
var agentSkills = await _db.GetAgentSkills(agent.AgentId);
var user = userId != null ? await _db.GetUser(userId) : null;
var systemPromptText = SystemPromptBuilder.Build(
agent, session, markedMessages, _memoryService, agentMemoryDir,
agentSkills, _skillService, userPersona: user?.GetEffectivePersona());
var workspace = session.Workspace;
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;
var chainId = Guid.NewGuid().ToString("N");
await _db.CreateChain(new ChainRecord
{
ChainId = chainId,
SessionId = sessionId,
AgentId = agent.AgentId,
Model = modelId,
SystemPrompt = systemPromptText,
ToolChoice = (hasWorkspace || includeMemory) ? toolChoice : null,
ToolNames = toolNames
});
await _db.UpdateSessionTimestamp(sessionId);
if (!string.IsNullOrEmpty(workspace))
Directory.CreateDirectory(workspace);
var systemPrompt = new List<object>();
if (!string.IsNullOrWhiteSpace(systemPromptText))
systemPrompt.Add(new { type = "text", text = systemPromptText });
return new ChatContext
{
ChainId = chainId,
SessionId = sessionId,
ModelId = modelId,
Workspace = workspace,
AgentId = agent.AgentId,
SystemPromptText = systemPromptText,
SystemPrompt = systemPrompt,
ToolChoice = toolChoice,
UseMemory = agent.UseMemory,
WebSearchVersion = agent.WebSearchVersion,
MaxIterations = maxIterations,
Agent = agent,
Session = session
};
}
/// <summary>
/// Baut die Message-History aus der DB (aktive Chains).
/// </summary>
public async Task<List<object>> BuildHistoryFromDb(string sessionId)
{
var chains = await _db.GetActiveChains(sessionId);
var history = new List<object>();
foreach (var chain in chains)
{
var msgs = await _db.GetChainMessages(chain.ChainId);
var grouped = new Dictionary<int, List<MessageRecord>>();
foreach (var msg in msgs)
{
if (!grouped.ContainsKey(msg.Iteration))
grouped[msg.Iteration] = new List<MessageRecord>();
grouped[msg.Iteration].Add(msg);
}
foreach (var kvp in grouped.OrderBy(x => x.Key))
{
foreach (var msg in kvp.Value)
{
if (msg.Role == "user" && msg.Type == "text")
{
var content = new List<object>();
var attachments = await _db.GetAttachments(msg.Id);
foreach (var att in attachments)
{
if (att.Type == "image")
{
content.Add(new
{
type = "image",
source = new { type = "base64", media_type = att.MediaType, data = att.Data }
});
}
else if (att.Type == "document")
{
content.Add(new
{
type = "document",
source = new { type = "base64", media_type = att.MediaType, data = att.Data }
});
}
}
content.Add(new { type = "text", text = msg.Content ?? "" });
history.Add(new Dictionary<string, object> { ["role"] = "user", ["content"] = content });
}
else if (msg.Role == "assistant" && msg.Type == "text")
{
history.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = new List<object> { new { type = "text", text = msg.Content ?? "" } }
});
}
else if (msg.Role == "assistant" && msg.Type == "tool_use")
{
JsonElement inputEl;
try { inputEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null;
if (lastAssistant is Dictionary<string, object> dict &&
dict.TryGetValue("role", out var role) && role.ToString() == "assistant" &&
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
{
contentList.Add(new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl });
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = new List<object>
{
new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl }
}
});
}
}
else if (msg.Role == "user" && msg.Type == "tool_result")
{
var lastUser = history.Count > 0 ? history[history.Count - 1] : null;
if (lastUser is Dictionary<string, object> dict &&
dict.TryGetValue("role", out var role) && role.ToString() == "user" &&
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
{
contentList.Add(new { type = "tool_result", tool_use_id = msg.ToolCallId, content = msg.Content ?? "", is_error = msg.IsError });
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = new List<object>
{
new { type = "tool_result", tool_use_id = msg.ToolCallId, content = msg.Content ?? "", is_error = msg.IsError }
}
});
}
}
}
}
}
return history;
}
}
+30 -59
View File
@@ -159,6 +159,9 @@ public class DatabaseService
// User-Zuordnung
TryAddColumn(conn, "sessions", "user_id", "TEXT");
TryAddColumn(conn, "sessions", "telegram_bot_token", "TEXT");
TryAddColumn(conn, "sessions", "telegram_webhook_secret", "TEXT");
TryAddColumn(conn, "sessions", "telegram_chat_id", "TEXT");
TryAddColumn(conn, "agents", "user_id", "TEXT");
TryAddColumn(conn, "users", "is_admin", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "users", "persona", "TEXT");
@@ -331,7 +334,9 @@ public class DatabaseService
s.CreatedAt,
s.UpdatedAt,
MessageCount = activeCount,
TotalMessageCount = totalCount
TotalMessageCount = totalCount,
TelegramConfigured = !string.IsNullOrEmpty(s.TelegramBotToken),
TelegramLinked = !string.IsNullOrEmpty(s.TelegramChatId)
});
}
return results;
@@ -809,79 +814,45 @@ public class DatabaseService
new { userId, settings, now = DateTime.UtcNow.ToString("o") });
}
// ── Telegram ──
// ── Telegram (Session-basiert) ──
public async Task<string> CreateTelegramLinkCode(string userId, string sessionId, string agentId)
public async Task<SessionRecord> GetSessionByTelegramChatId(long chatId)
{
using var conn = CreateConnection();
var code = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
var now = DateTime.UtcNow.ToString("o");
var expires = DateTime.UtcNow.AddMinutes(5).ToString("o");
return await conn.QuerySingleOrDefaultAsync<SessionRecord>(
"SELECT * FROM sessions WHERE telegram_chat_id = @chatId AND is_deleted = 0",
new { chatId = chatId.ToString() });
}
// Alten Code für diese Session löschen
await conn.ExecuteAsync(
"DELETE FROM telegram_links WHERE link_code IS NOT NULL AND user_id = @userId AND session_id = @sessionId",
new { userId, sessionId });
// Neuen Pending-Link-Code erstellen (noch ohne telegram_chat_id)
public async Task UpdateSessionTelegram(string sessionId, string botToken, string webhookSecret, string chatId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(@"
INSERT INTO telegram_links (telegram_chat_id, user_id, session_id, agent_id, link_code, expires_at, created_at, updated_at)
VALUES (0, @userId, @sessionId, @agentId, @code, @expires, @now, @now)",
new { userId, sessionId, agentId, code, expires, now });
return code;
UPDATE sessions SET telegram_bot_token = @botToken, telegram_webhook_secret = @webhookSecret,
telegram_chat_id = @chatId, updated_at = @now
WHERE session_id = @sessionId",
new { sessionId, botToken, webhookSecret, chatId, now = DateTime.UtcNow.ToString("o") });
}
public async Task<TelegramLinkRecord> RedeemTelegramLinkCode(string code, long chatId)
public async Task SetSessionTelegramChatId(string sessionId, string chatId)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
var link = await conn.QuerySingleOrDefaultAsync<TelegramLinkRecord>(
"SELECT * FROM telegram_links WHERE link_code = @code AND telegram_chat_id = 0",
new { code });
if (link == null) return null;
if (string.Compare(link.ExpiresAt, now, StringComparison.Ordinal) < 0) return null;
// Bestehende Verknüpfung für diese chatId entfernen
// Erst alte Verknüpfung mit dieser chatId entfernen (ein Telegram-Chat = eine Session)
await conn.ExecuteAsync(
"DELETE FROM telegram_links WHERE telegram_chat_id = @chatId",
new { chatId });
// Pending-Eintrag aktivieren
await conn.ExecuteAsync(@"
UPDATE telegram_links SET telegram_chat_id = @chatId, link_code = NULL, expires_at = NULL, updated_at = @now
WHERE id = @id",
new { chatId, now, id = link.Id });
link.TelegramChatId = chatId;
link.LinkCode = null;
return link;
"UPDATE sessions SET telegram_chat_id = NULL, updated_at = @now WHERE telegram_chat_id = @chatId",
new { chatId, now = DateTime.UtcNow.ToString("o") });
// Dann diese Session verknüpfen
await conn.ExecuteAsync(
"UPDATE sessions SET telegram_chat_id = @chatId, updated_at = @now WHERE session_id = @sessionId",
new { sessionId, chatId, now = DateTime.UtcNow.ToString("o") });
}
public async Task<TelegramLinkRecord> GetTelegramLink(long chatId)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<TelegramLinkRecord>(
"SELECT * FROM telegram_links WHERE telegram_chat_id = @chatId",
new { chatId });
}
public async Task UpdateTelegramSession(long chatId, string sessionId, string agentId)
public async Task ClearSessionTelegramChatId(string sessionId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE telegram_links SET session_id = @sessionId, agent_id = @agentId, updated_at = @now WHERE telegram_chat_id = @chatId",
new { chatId, sessionId, agentId, now = DateTime.UtcNow.ToString("o") });
}
public async Task DeleteTelegramLink(long chatId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"DELETE FROM telegram_links WHERE telegram_chat_id = @chatId",
new { chatId });
"UPDATE sessions SET telegram_chat_id = NULL, updated_at = @now WHERE session_id = @sessionId",
new { sessionId, now = DateTime.UtcNow.ToString("o") });
}
public async Task<IEnumerable<SessionRecord>> GetUserSessions(string userId)
File diff suppressed because it is too large. Load diff
+1 -3
View File
@@ -7,7 +7,5 @@
},
"AllowedHosts": "*",
"AnthropicToken": "",
"MaxIterations": 100,
"TelegramBotToken": "",
"TelegramWebhookSecret": ""
"MaxIterations": 100
}
-120
View File
@@ -255,123 +255,3 @@
background: rgb(from var(--text) r g b / .15);
color: var(--text);
}
/* Telegram Link Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgb(from var(--f1-bg) r g b / var(--semi-transparent));
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.telegram-link-modal {
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
width: 90vw;
max-width: 28em;
display: flex;
flex-direction: column;
overflow: hidden;
}
.telegram-link-modal .modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--box-padding);
border-bottom: var(--border-width) solid var(--f1-border);
background: var(--f1-bg);
}
.telegram-link-modal .modal-header h3 {
margin: 0;
display: flex;
align-items: center;
gap: var(--badge-spacing);
}
.telegram-link-modal .modal-body {
padding: var(--box-padding);
display: flex;
flex-direction: column;
gap: var(--box-spacing);
}
.telegram-code-display {
display: flex;
align-items: center;
justify-content: center;
gap: var(--box-spacing);
padding: var(--box-padding);
background: var(--f1-bg);
border: var(--border-width) solid var(--f1-border);
border-radius: var(--border-radius);
}
.telegram-code-display code {
font-size: calc(var(--font-size) * 1.5);
font-weight: bold;
letter-spacing: .1em;
color: var(--primary);
user-select: all;
}
.telegram-code-hint {
opacity: var(--semi-transparent);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: var(--badge-spacing);
}
/* ── Telegram Settings Status Box ── */
.telegram-status-box {
margin: 0 0 var(--box-spacing) 0;
background: var(--f3-bg);
border: var(--border-width) solid var(--f3-border);
border-radius: var(--border-radius);
padding: var(--box-padding);
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
}
.tg-status-row {
display: flex;
gap: var(--badge-spacing);
}
.tg-status-row.tg-error {
background: color-mix(in srgb, var(--danger) 8%, transparent);
}
.tg-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tg-value {
flex: 3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tg-badge-ok {
background: color-mix(in srgb, var(--success) 15%, transparent);
color: var(--success);
}
.tg-badge-off {
background: color-mix(in srgb, var(--warning) 15%, transparent);
color: var(--warning);
}
+20 -1
View File
@@ -51,7 +51,7 @@
.form-actions {
display: flex;
gap: var(--badge-spacing);
margin-top: var(--box-spacing);
margin-block: var(--box-spacing) calc(var(--page-spacing) * 4);
}
/* ── Custom Commands ── */
@@ -272,3 +272,22 @@ label.skill-item {
transform: translateX(1em);
}
/* ── Telegram Section ── */
.telegram-status-box {
padding: var(--box-padding);
background: var(--f3-bg);
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
}
.telegram-status-box .tg-error {
color: var(--dangerous);
}
.telegram-actions {
margin-top: var(--badge-spacing);
}
+39
View File
@@ -56,6 +56,45 @@
background: rgb(from var(--dangerous) r g b / .1);
}
/* Toast Notifications */
#toastContainer {
position: fixed;
left: 50%;
translate: -50% 0;
bottom: calc(var(--page-spacing) * 2);
z-index: 9999;
display: flex;
flex-direction: column-reverse;
align-items: center;
gap: var(--badge-spacing);
pointer-events: none;
width: calc(min(100%, var(--page-width)) - var(--page-spacing));
}
.toast {
pointer-events: auto;
padding: var(--box-padding);
border-radius: var(--border-radius);
background: color-mix(in srgb, var(--f1-bg), transparent 10%);
color: var(--text-color);
cursor: pointer;
opacity: 0;
transition: opacity var(--transition-duration);
}
.toast-visible {
opacity: 1;
}
.toast i {
margin-right: var(--badge-spacing);
}
.toast-success i { color: var(--primary); }
.toast-error i { color: var(--dangerous); }
.toast-info i { color: var(--secondary); }
/* Layout */
header {
+15 -31
View File
@@ -107,6 +107,20 @@
<label>Benutzerdefinierte Befehle</label>
<div class="custom-commands" id="customCommandsList"></div>
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
<label><i class="fa fa-paper-plane"></i> <b>Telegram Bot</b></label>
<span>Erstelle einen Bot via <a href="https://t.me/BotFather" target="_blank">@BotFather</a></span>
<label>Bot-Token</label>
<input type="text" id="sessionTelegramToken" placeholder="123456:ABC-DEF..." autocomplete="off">
<label>Webhook-Secret</label>
<input type="text" id="sessionTelegramSecret" readonly style="opacity: var(--semi-transparent); ">
<label>Status</label>
<div id="sessionTelegramStatusBox" class="telegram-status-box">Kein Status verfügbar</div>
<div class="telegram-actions">
<button type="button" id="sessionTelegramRegisterBtn" hidden>Webhook registrieren</button>
<button type="button" id="sessionTelegramDeleteBtn" class="danger-btn" hidden>Webhook entfernen</button>
</div>
<div class="form-actions">
<button id="sessionSaveBtn">Speichern</button>
<button id="sessionCancelBtn">Abbrechen</button>
@@ -300,37 +314,7 @@
</div>
</div>
</section>
<section id="settingsTelegramSection" class="settings-section" style="display:none">
<h2>Telegram Bot</h2>
<div class="setting-row">
<div class="setting-info">
<label>Status</label>
<span class="setting-hint">Verbindung zum Telegram Bot und Webhook-URL</span>
</div>
<div class="setting-control">
<button id="settingsTelegramStatusBtn">Status abrufen</button>
</div>
</div>
<div id="settingsTelegramStatusBox" class="telegram-status-box" style="display:none"></div>
<div class="setting-row">
<div class="setting-info">
<label>Webhook registrieren</label>
<span class="setting-hint">Registriert den Webhook bei Telegram (einmalig nach dem Deploy nötig)</span>
</div>
<div class="setting-control">
<button id="settingsTelegramRegisterBtn">Registrieren</button>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<label>Webhook entfernen</label>
<span class="setting-hint">Deregistriert den Webhook bei Telegram – Bot empfängt dann keine Updates mehr</span>
</div>
<div class="setting-control">
<button id="settingsTelegramDeleteBtn" class="danger-btn">Webhook entfernen</button>
</div>
</div>
</section>
<section class="settings-section settings-reset">
<button id="settingsResetBtn" class="danger-btn">Alle Einstellungen zurücksetzen</button>
</section>
+19 -19
View File
@@ -263,36 +263,30 @@ async function apiChangePassword(currentPassword, newPassword) {
return resp.json();
}
async function apiTelegramStatus() {
const resp = await fetch('/api/telegram/status');
// ── Telegram API (pro Session) ──
async function apiSessionTelegramGet(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`);
if (!resp.ok) return { configured: false };
return resp.json();
}
async function apiTelegramLink(sessionId) {
const resp = await fetch('/api/telegram/link', {
method: 'POST',
credentials: 'include',
async function apiSessionTelegramSave(sessionId, botToken, webhookSecret) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId })
body: JSON.stringify({ botToken, webhookSecret })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Telegram-Link fehlgeschlagen');
throw new Error(err.error || 'Telegram-Config speichern fehlgeschlagen');
}
return resp.json();
}
async function apiTelegramWebhookInfo() {
const resp = await fetch('/api/telegram/webhook-info', { credentials: 'include' });
if (!resp.ok) throw new Error('Webhook-Info fehlgeschlagen');
return resp.json();
}
async function apiTelegramRegisterWebhook(baseUrl) {
const resp = await fetch('/api/telegram/register-webhook', {
async function apiSessionTelegramRegisterWebhook(sessionId, baseUrl) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/register-webhook`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ baseUrl })
});
@@ -300,8 +294,14 @@ async function apiTelegramRegisterWebhook(baseUrl) {
return resp.json();
}
async function apiTelegramDeleteWebhook() {
const resp = await fetch('/api/telegram/webhook', { method: 'DELETE', credentials: 'include' });
async function apiSessionTelegramWebhookInfo(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook-info`);
if (!resp.ok) throw new Error('Webhook-Info fehlgeschlagen');
return resp.json();
}
async function apiSessionTelegramDeleteWebhook(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Webhook-Löschung fehlgeschlagen');
return resp.json();
}
+3 -56
View File
@@ -17,8 +17,7 @@ registerPage('dashboard', {
// Globaler Click-Handler zum Schließen aller Card-Menüs
document.addEventListener('click', this._closeCardMenus = () => closeAllCardMenus());
// Telegram-Status parallel laden
const telegramStatusPromise = apiTelegramStatus().catch(() => ({ configured: false }));
// Telegram-Config parallel pro Session laden (sessions werden gleich geladen)
const agents = await apiGetAgents();
const agentMap = {};
@@ -41,6 +40,7 @@ registerPage('dashboard', {
const sessionBadges = [];
if (agent) sessionBadges.push(`<span class="badge">${esc(agent.name)}</span>`);
if (s.context) sessionBadges.push(`<span class="badge badge-feature" title="${esc(s.context)}"><i class="fa fa-file-text-o"></i></span>`);
if (s.telegramConfigured) sessionBadges.push(`<span class="badge ${s.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${s.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
card.innerHTML = `
<div class="card-image">${s.image ? `<img src="${s.image}" alt="">` : ''}</div>
@@ -52,7 +52,6 @@ registerPage('dashboard', {
<button class="card-menu-trigger icon-btn"><i class="fa fa-ellipsis-v"></i></button>
<div class="card-menu-dropdown" hidden>
<button class="card-menu-item" data-action="chat"><i class="fa fa-comment"></i> Chat öffnen</button>
<button class="card-menu-item" data-action="telegram" hidden><i class="fa fa-paper-plane"></i> Telegram verknüpfen</button>
<button class="card-menu-item" data-action="edit"><i class="fa fa-pencil"></i> Bearbeiten</button>
</div>
</div>
@@ -98,18 +97,6 @@ registerPage('dashboard', {
menuDropdown.hidden = true;
navigateTo('chat', { id: s.sessionId });
});
const telegramBtn = menuDropdown.querySelector('[data-action="telegram"]');
telegramBtn.addEventListener('click', async (e) => {
e.stopPropagation();
menuDropdown.hidden = true;
try {
const result = await apiTelegramLink(s.sessionId);
showTelegramLinkCode(result.code, s.name || 'Unbenannt');
} catch (err) {
showToast(err.message, 'error');
}
});
menuDropdown.querySelector('[data-action="edit"]').addEventListener('click', (e) => {
e.stopPropagation();
menuDropdown.hidden = true;
@@ -156,12 +143,7 @@ registerPage('dashboard', {
}
}
// Telegram-Buttons einblenden wenn konfiguriert
telegramStatusPromise.then(status => {
if (status.configured) {
grid.querySelectorAll('[data-action="telegram"]').forEach(btn => btn.hidden = false);
}
});
// Neue Session Button
document.getElementById('newSessionBtn').addEventListener('click', () => navigateTo('session'));
@@ -262,42 +244,7 @@ registerPage('dashboard', {
}
});
function showTelegramLinkCode(code, sessionName) {
// Bestehendes Modal entfernen
const existing = document.getElementById('telegramLinkModal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.id = 'telegramLinkModal';
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal telegram-link-modal">
<div class="modal-header">
<h3><i class="fa fa-paper-plane"></i> Telegram verknüpfen</h3>
<button class="icon-btn modal-close"><i class="fa fa-times"></i></button>
</div>
<div class="modal-body">
<p>Sende diesen Code an deinen Telegram-Bot um die Session <strong>${esc(sessionName)}</strong> zu verknüpfen:</p>
<div class="telegram-code-display">
<code>/link ${esc(code)}</code>
<button class="icon-btn" title="Kopieren" id="copyTelegramCode"><i class="fa fa-copy"></i></button>
</div>
<p class="telegram-code-hint"><i class="fa fa-clock-o"></i> Der Code ist 5 Minuten gültig.</p>
</div>
</div>
`;
document.body.appendChild(modal);
modal.querySelector('.modal-close').addEventListener('click', () => modal.remove());
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
document.getElementById('copyTelegramCode').addEventListener('click', () => {
navigator.clipboard.writeText('/link ' + code).then(() => {
showToast('In Zwischenablage kopiert', 'success');
});
});
}
function closeAllCardMenus() {
document.querySelectorAll('.card-menu-dropdown').forEach(d => d.hidden = true);
+144
View File
@@ -55,6 +55,11 @@ registerPage('session', {
// Befehl hinzufügen
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
// Telegram-Section nur für existierende Sessions
if (!isNew) {
initSessionTelegram(sessionId);
}
// Agents laden
const agents = await apiGetAgents();
agentSelect.innerHTML = '<option value="">-- Kein Agent --</option>';
@@ -135,6 +140,17 @@ registerPage('session', {
await apiCreateSession(data);
} else {
await apiUpdateSession(sessionId, data);
// Telegram-Config mitspeichern (falls Section sichtbar)
var tgToken = document.getElementById('sessionTelegramToken');
var tgSecret = document.getElementById('sessionTelegramSecret');
if (tgToken) {
try {
await saveTelegramConfig(sessionId, tgToken, tgSecret);
} catch(e) {
showToast('Session gespeichert, aber Telegram-Fehler: ' + e.message, 'error');
return;
}
}
}
navigateTo('dashboard');
});
@@ -157,3 +173,131 @@ registerPage('session', {
_sessionImageData = null;
}
});
function initSessionTelegram(sessionId) {
var tokenInput = document.getElementById('sessionTelegramToken');
var secretInput = document.getElementById('sessionTelegramSecret');
var registerBtn = document.getElementById('sessionTelegramRegisterBtn');
var deleteBtn = document.getElementById('sessionTelegramDeleteBtn');
var statusBox = document.getElementById('sessionTelegramStatusBox');
// Vorhandene Config laden + Status abrufen
apiSessionTelegramGet(sessionId).then(async function(config) {
if (config.botToken) tokenInput.value = config.botToken;
if (config.webhookSecret) {
secretInput.value = config.webhookSecret;
} else {
// Secret einmalig generieren und sofort persistent speichern
var newSecret = crypto.randomUUID().replace(/-/g, '');
secretInput.value = newSecret;
await apiSessionTelegramSave(sessionId, config.botToken || '', newSecret);
}
// Status automatisch abrufen wenn Token konfiguriert ist
if (config.botToken) {
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
} else {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
}
}).catch(function() {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
});
// Webhook registrieren
registerBtn.addEventListener('click', async function() {
var baseUrl = prompt('Base-URL der App:', window.location.origin);
if (!baseUrl) return;
registerBtn.disabled = true;
registerBtn.textContent = 'Wird registriert...';
try {
// Erst Token + Secret speichern (falls geändert)
await saveTelegramConfig(sessionId, tokenInput, secretInput);
var res = await apiSessionTelegramRegisterWebhook(sessionId, baseUrl.trim());
if (res.ok) {
showToast('Webhook registriert!', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
registerBtn.disabled = false;
registerBtn.textContent = 'Webhook registrieren';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
// Webhook entfernen
deleteBtn.addEventListener('click', async function() {
if (!confirm('Webhook wirklich entfernen?')) return;
deleteBtn.disabled = true;
deleteBtn.textContent = 'Wird entfernt...';
try {
var res = await apiSessionTelegramDeleteWebhook(sessionId);
if (res.ok) {
showToast('Webhook entfernt', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
deleteBtn.disabled = false;
deleteBtn.textContent = 'Webhook entfernen';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
}
function showTelegramUnconfigured(statusBox, registerBtn, deleteBtn) {
statusBox.innerHTML = '<p>⚠️ Kein Bot-Token konfiguriert. Speichere zuerst die Session mit einem Token.</p>';
statusBox.hidden = false;
registerBtn.hidden = true;
deleteBtn.hidden = true;
}
async function refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn) {
statusBox.hidden = true;
try {
var info = await apiSessionTelegramWebhookInfo(sessionId);
var result = info.result || {};
var url = result.url || '';
var pending = result.pending_update_count || 0;
var lastError = result.last_error_message;
var active = !!url;
var html = '';
if (active) {
html += '<p>✅ <strong>Webhook aktiv</strong></p>';
html += '<p><strong>URL:</strong> ' + escapeHtml(url) + '</p>';
html += '<p><strong>Ausstehend:</strong> ' + pending + '</p>';
if (lastError) html += '<p class="tg-error"><strong>Letzter Fehler:</strong> ' + escapeHtml(lastError) + '</p>';
} else {
html += '<p>⚠️ <strong>Webhook nicht registriert</strong></p>';
}
statusBox.innerHTML = html;
statusBox.hidden = false;
// Buttons ein/ausblenden je nach Status
registerBtn.hidden = active;
deleteBtn.hidden = !active;
} catch(e) {
statusBox.innerHTML = '<p class="tg-error">Status konnte nicht abgerufen werden: ' + escapeHtml(e.message) + '</p>';
statusBox.hidden = false;
registerBtn.hidden = false;
deleteBtn.hidden = true;
}
}
async function saveTelegramConfig(sessionId, tokenInput, secretInput) {
var token = tokenInput.value.trim();
var secret = secretInput.value.trim();
await apiSessionTelegramSave(sessionId, token, secret);
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
+2 -91
View File
@@ -9,12 +9,10 @@ registerPage('settings', {
const showDeletedToggle = document.getElementById('settingShowDeletedMessages');
const resetBtn = document.getElementById('settingsResetBtn');
const backupSection = document.getElementById('settingsBackupSection');
const telegramSection = document.getElementById('settingsTelegramSection');
// Backup + Telegram nur für Admins
// Backup nur für Admins
if (currentUser && currentUser.isAdmin) {
backupSection.style.display = '';
initTelegramSection();
}
const s = loadSettings();
@@ -77,96 +75,9 @@ registerPage('settings', {
showDeletedToggle.checked = SETTINGS_DEFAULTS.showDeletedMessages;
});
function initTelegramSection() {
// Erst prüfen ob Telegram überhaupt konfiguriert ist
apiTelegramStatus().then(s => {
if (!s.configured) return; // Bot-Token nicht gesetzt → Section versteckt lassen
telegramSection.style.display = '';
const statusBtn = document.getElementById('settingsTelegramStatusBtn');
const statusBox = document.getElementById('settingsTelegramStatusBox');
const registerBtn = document.getElementById('settingsTelegramRegisterBtn');
const deleteBtn = document.getElementById('settingsTelegramDeleteBtn');
// Status abrufen
statusBtn.addEventListener('click', async () => {
statusBtn.disabled = true;
statusBtn.textContent = 'Wird abgerufen...';
statusBox.style.display = 'none';
try {
const info = await apiTelegramWebhookInfo();
const result = info.result || {};
const url = result.url || '(kein Webhook registriert)';
const pending = result.pending_update_count ?? 0;
const lastError = result.last_error_message;
let html = `<div class="tg-status-row"><span class="tg-label">Webhook-URL</span><span class="tg-value">${escapeHtml(url)}</span></div>`;
html += `<div class="tg-status-row"><span class="tg-label">Ausstehende Updates</span><span class="tg-value">${pending}</span></div>`;
if (lastError) {
html += `<div class="tg-status-row tg-error"><span class="tg-label">Letzter Fehler</span><span class="tg-value">${escapeHtml(lastError)}</span></div>`;
}
const active = !!result.url;
html += `<div class="tg-status-row"><span class="tg-label">Status</span><span class="tg-value tg-badge ${active ? 'tg-badge-ok' : 'tg-badge-off'}">${active ? '✅ Aktiv' : '⚠️ Nicht registriert'}</span></div>`;
statusBox.innerHTML = html;
statusBox.style.display = '';
} catch (e) {
statusBox.innerHTML = `<div class="tg-status-row tg-error"><span class="tg-value">Fehler: ${escapeHtml(e.message)}</span></div>`;
statusBox.style.display = '';
} finally {
statusBtn.disabled = false;
statusBtn.textContent = 'Status abrufen';
}
});
// Webhook registrieren
registerBtn.addEventListener('click', async () => {
const baseUrl = prompt('Base-URL der App (z.B. ' + window.location.origin + '):', window.location.origin);
if (!baseUrl) return;
registerBtn.disabled = true;
registerBtn.textContent = 'Wird registriert...';
try {
const res = await apiTelegramRegisterWebhook(baseUrl.trim());
if (res.result && res.result.ok) {
alert('✅ Webhook erfolgreich registriert!');
} else {
alert('Antwort: ' + JSON.stringify(res.result));
}
} catch (e) {
alert('Fehler: ' + e.message);
} finally {
registerBtn.disabled = false;
registerBtn.textContent = 'Registrieren';
}
});
// Webhook löschen
deleteBtn.addEventListener('click', async () => {
if (!confirm('Webhook wirklich entfernen? Der Bot empfängt dann keine Nachrichten mehr.')) return;
deleteBtn.disabled = true;
deleteBtn.textContent = 'Wird entfernt...';
try {
const res = await apiTelegramDeleteWebhook();
if (res.ok) {
alert('✅ Webhook erfolgreich entfernt.');
} else {
alert('Antwort: ' + JSON.stringify(res));
}
} catch (e) {
alert('Fehler: ' + e.message);
} finally {
deleteBtn.disabled = false;
deleteBtn.textContent = 'Webhook entfernen';
}
});
});
}
},
destroy() {}
});
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
+30
View File
@@ -1,3 +1,33 @@
// ── Toast Notifications ──
function showToast(message, type) {
type = type || 'info';
var container = document.getElementById('toastContainer');
if (!container) {
container = document.createElement('div');
container.id = 'toastContainer';
document.body.appendChild(container);
}
var toast = document.createElement('div');
toast.className = 'toast toast-' + type;
var icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-times-circle' : 'fa-info-circle';
toast.innerHTML = '<i class="fa ' + icon + '"></i> ' + (message || '');
container.appendChild(toast);
// Einblenden
requestAnimationFrame(function() { toast.classList.add('toast-visible'); });
// Automatisch ausblenden
var timeout = setTimeout(function() { removeToast(toast); }, 4000);
toast.addEventListener('click', function() {
clearTimeout(timeout);
removeToast(toast);
});
}
function removeToast(toast) {
toast.classList.remove('toast-visible');
toast.addEventListener('transitionend', function() { toast.remove(); });
}
// ── DOM Helpers ──
const $ = (sel) => document.querySelector(sel);