Backend:
- is_active komplett entfernt, durch is_public (Admin-Toggle) ersetzt
- Neues selected_api_key_id auf users-Tabelle
- AnthropicService: globaler _token entfernt, apiToken pro Request
- Token-Durchreichung: StreamChat → ProcessStream → RunAgentChat →
RunPreparer/RunReview/RunFinalizer, ExecuteTool, send_to_session
- ChatEndpoint: User-Key aus DB laden, an StreamChat übergeben
- TelegramService: Key per bot.UserId auflösen
- CronjobService: Key per job.UserId auflösen
- Neue Endpoints: GET /api/api-keys/available, PUT /api/api-keys/select/{keyId},
GET /api/api-keys/selected, PUT /api/admin/api-keys/{keyId}/public,
POST /api/admin/api-keys/check-all
- Program.cs: AnthropicService ohne Token-Parameter
Frontend:
- API-Keys-Page: Public-Toggle (Globe/Lock) statt Activate/Deactivate
- Alle prüfen Button für Batch-Key-Check
- Chat: Key-Auswahl für alle User (nicht nur Admin)
- Dummy-Badge wenn kein Key ausgewählt
- Fallback-Badge bei Key ohne Usage-Daten
- Key-Switch-Modal: apiGetAvailableKeys + apiSelectApiKey
215 lines
7.5 KiB
C#
215 lines
7.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Cronos;
|
|
using Gott.Models;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Gott.Services;
|
|
|
|
public class CronjobService : BackgroundService
|
|
{
|
|
private readonly DatabaseService _db;
|
|
private readonly ChatService _chatService;
|
|
private readonly AnthropicService _anthropicService;
|
|
private readonly TelegramService _telegramService;
|
|
private readonly ILogger<CronjobService> _logger;
|
|
|
|
public CronjobService(
|
|
DatabaseService db,
|
|
ChatService chatService,
|
|
AnthropicService anthropicService,
|
|
TelegramService telegramService,
|
|
ILogger<CronjobService> logger)
|
|
{
|
|
_db = db;
|
|
_chatService = chatService;
|
|
_anthropicService = anthropicService;
|
|
_telegramService = telegramService;
|
|
_logger = logger;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation("CronjobService gestartet");
|
|
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await ProcessDueCronjobs();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Fehler beim Verarbeiten der Cronjobs");
|
|
}
|
|
|
|
// Jede Minute prüfen
|
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
|
}
|
|
}
|
|
|
|
private async Task ProcessDueCronjobs()
|
|
{
|
|
var dueJobs = await _db.GetDueCronjobs();
|
|
|
|
foreach (var job in dueJobs)
|
|
{
|
|
_logger.LogInformation("Cronjob '{Name}' ({Id}) wird ausgeführt", job.Name, job.CronjobId);
|
|
|
|
try
|
|
{
|
|
await ExecuteCronjob(job);
|
|
|
|
var nextRun = CalculateNextRun(job.CronExpression);
|
|
await _db.UpdateCronjobRun(job.CronjobId, "success", null, nextRun);
|
|
|
|
_logger.LogInformation("Cronjob '{Name}' erfolgreich. Nächste Ausführung: {Next}", job.Name, nextRun);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Cronjob '{Name}' fehlgeschlagen", job.Name);
|
|
|
|
var nextRun = CalculateNextRun(job.CronExpression);
|
|
await _db.UpdateCronjobRun(job.CronjobId, "error", ex.Message, nextRun);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task ExecuteCronjob(CronjobRecord job)
|
|
{
|
|
// Chat-Context vorbereiten (wie bei normalem Chat)
|
|
var ctx = await _chatService.PrepareContext(job.SessionId, job.UserId);
|
|
if (ctx == null)
|
|
throw new Exception("Session oder Agent nicht gefunden / nicht konfiguriert");
|
|
|
|
// API-Key des Cronjob-Owners auflösen
|
|
var userApiKey = await _db.GetUserSelectedApiKey(job.UserId);
|
|
if (userApiKey == null)
|
|
throw new Exception($"Kein API-Key für User {job.UserId} ausgewählt");
|
|
ctx.ApiToken = userApiKey.ApiKey;
|
|
|
|
// History aus DB laden
|
|
var history = await _chatService.BuildHistoryFromDb(job.SessionId, promptBlocks: ctx.PromptBlocks);
|
|
|
|
// User-Nachricht hinzufügen
|
|
var userContent = new List<object> { new { type = "text", text = job.Prompt } };
|
|
history.Add(new Dictionary<string, object> { ["role"] = "user", ["content"] = userContent });
|
|
|
|
// User-Nachricht in DB speichern
|
|
await _db.SaveMessage(new MessageRecord
|
|
{
|
|
ChainId = ctx.ChainId,
|
|
Iteration = 0,
|
|
Role = "user",
|
|
Type = "text",
|
|
Content = job.Prompt
|
|
});
|
|
|
|
// Non-Streaming Chat ausführen
|
|
var responseText = await _anthropicService.ChatNonStreaming(ctx, history);
|
|
|
|
// Session-Timestamp aktualisieren
|
|
await _db.UpdateSessionTimestamp(job.SessionId);
|
|
|
|
// Telegram-Benachrichtigung auf der Arbeits-Session senden, falls sie Telegram hat
|
|
if (!string.IsNullOrEmpty(responseText))
|
|
{
|
|
await _telegramService.SendNotification(job.SessionId,
|
|
$"⏰ Cronjob: {job.Name}\n📂 Session: {job.SessionName}\n\n{responseText}");
|
|
}
|
|
|
|
// Benachrichtigungs-Session: Zusammenfassung in eine andere Session schreiben
|
|
if (!string.IsNullOrEmpty(job.NotifySessionId) && job.NotifySessionId != job.SessionId)
|
|
{
|
|
await SendCronjobNotification(job, responseText);
|
|
}
|
|
}
|
|
|
|
private async Task SendCronjobNotification(CronjobRecord job, string responseText)
|
|
{
|
|
try
|
|
{
|
|
var notifyCtx = await _chatService.PrepareContext(job.NotifySessionId, job.UserId);
|
|
if (notifyCtx == null)
|
|
{
|
|
_logger.LogWarning("Notification-Session {SessionId} für Cronjob '{Name}' nicht gefunden", job.NotifySessionId, job.Name);
|
|
return;
|
|
}
|
|
|
|
// API-Key des Cronjob-Owners auflösen
|
|
var notifyApiKey = await _db.GetUserSelectedApiKey(job.UserId);
|
|
if (notifyApiKey == null)
|
|
{
|
|
_logger.LogWarning("Kein API-Key für User {UserId} (Cronjob '{Name}' Notification)", job.UserId, job.Name);
|
|
return;
|
|
}
|
|
notifyCtx.ApiToken = notifyApiKey.ApiKey;
|
|
|
|
// Kurze Zusammenfassung als User-Nachricht in die Notify-Session schreiben
|
|
var summary = $"[Cronjob \"{job.Name}\" wurde in Session \"{job.SessionName}\" ausgeführt]\n\nAntwort:\n{Truncate(responseText, 2000)}";
|
|
|
|
// In Notify-Session als Nachricht speichern
|
|
await _db.SaveMessage(new MessageRecord
|
|
{
|
|
ChainId = notifyCtx.ChainId,
|
|
Iteration = 0,
|
|
Role = "user",
|
|
Type = "text",
|
|
Content = summary
|
|
});
|
|
|
|
// Nur die Benachrichtigung als History senden (nicht volle Session-History,
|
|
// da diese mit einer assistant-Nachricht enden könnte → prefill-Fehler)
|
|
var notifyHistory = new List<object>
|
|
{
|
|
new Dictionary<string, object>
|
|
{
|
|
["role"] = "user",
|
|
["content"] = new List<object> { new { type = "text", text = summary } }
|
|
}
|
|
};
|
|
var notifyResponse = await _anthropicService.ChatNonStreaming(notifyCtx, notifyHistory);
|
|
|
|
await _db.UpdateSessionTimestamp(job.NotifySessionId);
|
|
|
|
// Telegram auf Notify-Session senden
|
|
if (!string.IsNullOrEmpty(notifyResponse))
|
|
{
|
|
await _telegramService.SendNotification(job.NotifySessionId,
|
|
$"📋 Cronjob: {job.Name}\n📂 Session: {job.SessionName}\n\n{notifyResponse}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Fehler beim Senden der Cronjob-Benachrichtigung an Session {SessionId}", job.NotifySessionId);
|
|
}
|
|
}
|
|
|
|
private static string Truncate(string text, int maxLength)
|
|
{
|
|
if (string.IsNullOrEmpty(text) || text.Length <= maxLength) return text ?? "";
|
|
return text.Substring(0, maxLength) + "…";
|
|
}
|
|
|
|
public static string CalculateNextRun(string cronExpression)
|
|
{
|
|
try
|
|
{
|
|
var cron = CronExpression.Parse(cronExpression, CronFormat.Standard);
|
|
// Cron-Expression in Server-Lokalzeit interpretieren
|
|
var next = cron.GetNextOccurrence(DateTimeOffset.Now, TimeZoneInfo.Local);
|
|
return next?.UtcDateTime.ToString("o") ?? "";
|
|
}
|
|
catch
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
|
|
}
|