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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+160
-717
File diff suppressed because it is too large.
Load diff
+1
-3
@@ -7,7 +7,5 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AnthropicToken": "",
|
||||
"MaxIterations": 100,
|
||||
"TelegramBotToken": "",
|
||||
"TelegramWebhookSecret": ""
|
||||
"MaxIterations": 100
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user