Cronjobs + AnthropicService Singleton Refactoring
Cronjobs: - CronjobService (BackgroundService) prüft jede Minute auf fällige Jobs - Cron-Expression in Server-Lokalzeit via Cronos (CronFormat.Standard) - Cronjob-Ausführung: Prompt → Session → Agent-Antwort in Chat-History - Optionale Benachrichtigungs-Session (notify_session_id): Zusammenfassung wird in separate Session geschrieben (z.B. mit Telegram) - Telegram-Benachrichtigung mit Cronjob-Name + Ursprungs-Session - REST API: CRUD + toggle + manuelles Ausführen (/run) - Frontend: Dashboard-Cards, Cronjob-Formular mit Cron-Vorschau, Session-Dropdown, Notify-Session-Dropdown, Status-Box Refactoring: - AnthropicService als Singleton in DI registriert - ChatEndpoint + TelegramService nutzen injizierte Instanz - Redundante Dependencies (Token, ToolService, MemoryService) entfernt Neue Dateien: - Models/CronjobRecord.cs - Services/CronjobService.cs - Endpoints/CronjobEndpoint.cs - wwwroot/js/pages/cronjob-page.js - PLAN-Cronjob.md
This commit is contained in:
1 parent
0120bb8f73
commit
b645625b56
16 files changed
+1132
-28
No files matched your search
@@ -0,0 +1,199 @@
|
||||
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");
|
||||
|
||||
// History aus DB laden
|
||||
var history = await _chatService.BuildHistoryFromDb(job.SessionId);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -166,6 +166,7 @@ public class DatabaseService
|
||||
TryAddColumn(conn, "users", "is_admin", "INTEGER NOT NULL DEFAULT 0");
|
||||
TryAddColumn(conn, "users", "persona", "TEXT");
|
||||
TryAddColumn(conn, "users", "settings", "TEXT");
|
||||
TryAddColumn(conn, "cronjobs", "notify_session_id", "TEXT");
|
||||
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS agent_skills (
|
||||
@@ -205,6 +206,28 @@ public class DatabaseService
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_links_code ON telegram_links(link_code);
|
||||
");
|
||||
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS cronjobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cronjob_id TEXT NOT NULL UNIQUE,
|
||||
user_id TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
cron_expression TEXT NOT NULL,
|
||||
prompt TEXT NOT NULL,
|
||||
notify_session_id TEXT,
|
||||
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
last_run_at TEXT,
|
||||
next_run_at TEXT,
|
||||
last_run_status TEXT,
|
||||
last_run_error TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_cronjobs_user ON cronjobs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cronjobs_next_run ON cronjobs(next_run_at);
|
||||
");
|
||||
}
|
||||
|
||||
private void TryAddColumn(SqliteConnection conn, string table, string column, string type)
|
||||
@@ -862,4 +885,87 @@ public class DatabaseService
|
||||
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC",
|
||||
new { userId });
|
||||
}
|
||||
|
||||
// ── Cronjobs ──
|
||||
|
||||
public async Task<CronjobRecord> CreateCronjob(CronjobRecord cronjob)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
cronjob.CronjobId = Guid.NewGuid().ToString("N");
|
||||
cronjob.CreatedAt = now;
|
||||
cronjob.UpdatedAt = now;
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO cronjobs (cronjob_id, user_id, session_id, name, cron_expression, prompt, notify_session_id, is_enabled, next_run_at, created_at, updated_at)
|
||||
VALUES (@CronjobId, @UserId, @SessionId, @Name, @CronExpression, @Prompt, @NotifySessionId, @IsEnabled, @NextRunAt, @CreatedAt, @UpdatedAt)
|
||||
", cronjob);
|
||||
return cronjob;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CronjobRecord>> GetCronjobs(string userId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QueryAsync<CronjobRecord>(@"
|
||||
SELECT c.*, s.name AS session_name, ns.name AS notify_session_name
|
||||
FROM cronjobs c
|
||||
LEFT JOIN sessions s ON c.session_id = s.session_id
|
||||
LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id
|
||||
WHERE c.user_id = @userId
|
||||
ORDER BY c.name",
|
||||
new { userId });
|
||||
}
|
||||
|
||||
public async Task<CronjobRecord> GetCronjob(string cronjobId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QuerySingleOrDefaultAsync<CronjobRecord>(@"
|
||||
SELECT c.*, s.name AS session_name, ns.name AS notify_session_name
|
||||
FROM cronjobs c
|
||||
LEFT JOIN sessions s ON c.session_id = s.session_id
|
||||
LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id
|
||||
WHERE c.cronjob_id = @cronjobId",
|
||||
new { cronjobId });
|
||||
}
|
||||
|
||||
public async Task UpdateCronjob(CronjobRecord cronjob)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
cronjob.UpdatedAt = DateTime.UtcNow.ToString("o");
|
||||
await conn.ExecuteAsync(@"
|
||||
UPDATE cronjobs SET name = @Name, session_id = @SessionId, cron_expression = @CronExpression,
|
||||
prompt = @Prompt, notify_session_id = @NotifySessionId, is_enabled = @IsEnabled, next_run_at = @NextRunAt, updated_at = @UpdatedAt
|
||||
WHERE cronjob_id = @CronjobId AND user_id = @UserId",
|
||||
cronjob);
|
||||
}
|
||||
|
||||
public async Task DeleteCronjob(string cronjobId, string userId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
await conn.ExecuteAsync(
|
||||
"DELETE FROM cronjobs WHERE cronjob_id = @cronjobId AND user_id = @userId",
|
||||
new { cronjobId, userId });
|
||||
}
|
||||
|
||||
public async Task UpdateCronjobRun(string cronjobId, string status, string error, string nextRunAt)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
await conn.ExecuteAsync(@"
|
||||
UPDATE cronjobs SET last_run_at = @now, last_run_status = @status,
|
||||
last_run_error = @error, next_run_at = @nextRunAt, updated_at = @now
|
||||
WHERE cronjob_id = @cronjobId",
|
||||
new { cronjobId, status, error, nextRunAt, now = DateTime.UtcNow.ToString("o") });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CronjobRecord>> GetDueCronjobs()
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
return await conn.QueryAsync<CronjobRecord>(@"
|
||||
SELECT c.*, s.name AS session_name, ns.name AS notify_session_name
|
||||
FROM cronjobs c
|
||||
LEFT JOIN sessions s ON c.session_id = s.session_id
|
||||
LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id
|
||||
WHERE c.is_enabled = 1 AND c.next_run_at IS NOT NULL AND c.next_run_at <= @now",
|
||||
new { now });
|
||||
}
|
||||
}
|
||||
@@ -10,29 +10,23 @@ namespace Gott.Services;
|
||||
|
||||
public class TelegramService
|
||||
{
|
||||
private readonly string _anthropicToken;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly DatabaseService _db;
|
||||
private readonly ToolService _toolService;
|
||||
private readonly MemoryService _memoryService;
|
||||
private readonly ChatService _chatService;
|
||||
private readonly AnthropicService _anthropic;
|
||||
private readonly int _maxIterations;
|
||||
|
||||
public TelegramService(
|
||||
string anthropicToken,
|
||||
IHttpClientFactory httpFactory,
|
||||
DatabaseService db,
|
||||
ToolService toolService,
|
||||
MemoryService memoryService,
|
||||
ChatService chatService,
|
||||
AnthropicService anthropic,
|
||||
int maxIterations = 100)
|
||||
{
|
||||
_anthropicToken = anthropicToken;
|
||||
_httpFactory = httpFactory;
|
||||
_db = db;
|
||||
_toolService = toolService;
|
||||
_memoryService = memoryService;
|
||||
_chatService = chatService;
|
||||
_anthropic = anthropic;
|
||||
_maxIterations = maxIterations;
|
||||
}
|
||||
|
||||
@@ -165,8 +159,7 @@ public class TelegramService
|
||||
});
|
||||
|
||||
// AnthropicService für Non-Streaming-Chat nutzen
|
||||
var anthropic = new AnthropicService(_httpFactory, _anthropicToken, _toolService, _db, _memoryService);
|
||||
var reply = await anthropic.ChatNonStreaming(ctx, history, onIteration: async (iteration) =>
|
||||
var reply = await _anthropic.ChatNonStreaming(ctx, history, onIteration: async (iteration) =>
|
||||
{
|
||||
if (iteration % 3 == 0)
|
||||
await SendChatAction(chatId, botToken, "typing");
|
||||
|
||||
Reference in new issue
Block a user