i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -13,18 +13,18 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
public string Name => "SocialMediaManager";
|
||||
|
||||
public string Description => """
|
||||
Manager für Social Media Monitoring (X/Twitter, Reddit) und YouTube Transkripte.
|
||||
Manager for Social Media Monitoring (X/Twitter, Reddit) and YouTube Transcripts.
|
||||
|
||||
Aktionen:
|
||||
- x_search: Freitext-Suche auf X/Twitter
|
||||
- x_user_tweets: Tweets eines bestimmten Benutzers abrufen (username angeben)
|
||||
- reddit_search: Freitext-Suche auf Reddit
|
||||
- reddit_subreddit: Neue Posts aus einem bestimmten Subreddit abrufen (subreddit angeben)
|
||||
- youtube_transcript: YouTube-Video transkribieren
|
||||
Actions:
|
||||
- x_search: Free-text search on X/Twitter
|
||||
- x_user_tweets: Retrieve tweets from a specific user (specify username)
|
||||
- reddit_search: Free-text search on Reddit
|
||||
- reddit_subreddit: Retrieve new posts from a specific subreddit (specify subreddit)
|
||||
- youtube_transcript: Transcribe a YouTube video
|
||||
|
||||
Background-Jobs (konfiguriert in toolJobs):
|
||||
- sm_x_account_monitor: Überwacht konfigurierte X-Accounts auf neue Tweets (Config: xWatchAccounts)
|
||||
- sm_reddit_monitor: Überwacht konfigurierte Subreddits auf neue Posts (Config: redditWatchSubreddits)
|
||||
Background jobs (configured in toolJobs):
|
||||
- sm_x_account_monitor: Monitors configured X accounts for new tweets (Config: xWatchAccounts)
|
||||
- sm_reddit_monitor: Monitors configured subreddits for new posts (Config: redditWatchSubreddits)
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
@@ -34,27 +34,27 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["x_search", "x_user_tweets", "reddit_search", "reddit_subreddit", "youtube_transcript"],
|
||||
"description": "Die auszuführende Aktion"
|
||||
"description": "The action to execute"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Suchbegriff für x_search oder reddit_search"
|
||||
"description": "Search term for x_search or reddit_search"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"description": "X/Twitter Username ohne @ (nur für x_user_tweets)"
|
||||
"description": "X/Twitter username without @ (only for x_user_tweets)"
|
||||
},
|
||||
"subreddit": {
|
||||
"type": "string",
|
||||
"description": "Subreddit-Name ohne r/ (nur für reddit_subreddit)"
|
||||
"description": "Subreddit name without r/ (only for reddit_subreddit)"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "YouTube Video URL oder Video-ID (nur für youtube_transcript)"
|
||||
"description": "YouTube video URL or video ID (only for youtube_transcript)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximale Anzahl an Ergebnissen (Standard: 10)"
|
||||
"description": "Maximum number of results (default: 10)"
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
@@ -67,7 +67,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
var action = input.GetProperty("action").GetString() ?? throw new ArgumentException("Action is required");
|
||||
|
||||
// WorkspacePath per-agent scoped im StateStore für Fallback speichern
|
||||
// Save per-agent scoped WorkspacePath in StateStore for fallback
|
||||
await context.StateStore.SetAsync($"sm_workspace_path:{context.AgentId}", context.WorkspacePath!, ct);
|
||||
|
||||
try
|
||||
@@ -79,13 +79,13 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
"reddit_search" => await HandleRedditSearchAsync(input, context, ct),
|
||||
"reddit_subreddit" => await HandleRedditSubredditAsync(input, context, ct),
|
||||
"youtube_transcript" => await HandleYoutubeTranscriptAsync(input, context, ct),
|
||||
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
|
||||
_ => ToolResult.Fail($"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Logger.LogError(ex, "Fehler in SocialMediaManager ExecuteAsync ({Action})", action);
|
||||
return ToolResult.Fail($"Fehler: {ex.Message}");
|
||||
context.Logger.LogError(ex, "Error in SocialMediaManager ExecuteAsync ({Action})", action);
|
||||
return ToolResult.Fail($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var limit = input.TryGetProperty("limit", out var l) ? l.GetInt32() : 10;
|
||||
|
||||
var apiKey = context.ToolConfig.GetValueOrDefault("xApiKey")?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(apiKey)) return ToolResult.Fail("X API Key fehlt.");
|
||||
if (string.IsNullOrWhiteSpace(apiKey)) return ToolResult.Fail("X API key missing.");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.twitter.com/2/tweets/search/recent?query={Uri.EscapeDataString(query ?? "")}&max_results={limit}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
@@ -103,7 +103,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var response = await _httpClient.SendAsync(request, ct);
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode) return ToolResult.Fail($"X API Fehler: {response.StatusCode} - {content}");
|
||||
if (!response.IsSuccessStatusCode) return ToolResult.Fail($"X API error: {response.StatusCode} - {content}");
|
||||
|
||||
return ToolResult.Ok(content);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var response = await _httpClient.SendAsync(request, ct);
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode) return ToolResult.Fail($"Reddit API Fehler: {response.StatusCode}");
|
||||
if (!response.IsSuccessStatusCode) return ToolResult.Fail($"Reddit API error: {response.StatusCode}");
|
||||
|
||||
return ToolResult.Ok(content);
|
||||
}
|
||||
@@ -131,7 +131,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var username = input.TryGetProperty("username", out var u) ? u.GetString() : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
return ToolResult.Fail("'username' ist erforderlich (ohne @).");
|
||||
return ToolResult.Fail("'username' is required (without @).");
|
||||
|
||||
var cleanName = username.TrimStart('@').ToLowerInvariant();
|
||||
|
||||
@@ -165,14 +165,14 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
if (cachedTweets.Count > 0)
|
||||
{
|
||||
return ToolResult.Ok(
|
||||
$"[Aus lokalem Cache — kein API-Call nötig, {cachedTweets.Count} Tweets]\n" +
|
||||
$"[From local cache — no API call needed, {cachedTweets.Count} tweets]\n" +
|
||||
JsonSerializer.Serialize(cachedTweets, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Kein Cache vorhanden → Einmalige Abfrage über die Search API (1 API-Call)
|
||||
var apiKey = context.ToolConfig.GetValueOrDefault("xApiKey")?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(apiKey)) return ToolResult.Fail("X API Key (xApiKey) fehlt in der Tool-Config.");
|
||||
if (string.IsNullOrWhiteSpace(apiKey)) return ToolResult.Fail("X API key (xApiKey) missing in tool config.");
|
||||
|
||||
var limit = input.TryGetProperty("limit", out var l) ? l.GetInt32() : 10;
|
||||
var searchUrl = $"https://api.twitter.com/2/tweets/search/recent" +
|
||||
@@ -187,9 +187,9 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return ToolResult.Fail($"X API Fehler: {response.StatusCode} - {content}");
|
||||
return ToolResult.Fail($"X API error: {response.StatusCode} - {content}");
|
||||
|
||||
return ToolResult.Ok($"[1 API-Call verbraucht]\n{content}");
|
||||
return ToolResult.Ok($"[1 API call used]\n{content}");
|
||||
}
|
||||
|
||||
private async Task<ToolResult> HandleRedditSubredditAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
@@ -198,7 +198,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var limit = input.TryGetProperty("limit", out var l) ? l.GetInt32() : 10;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subreddit))
|
||||
return ToolResult.Fail("'subreddit' ist erforderlich (ohne r/).");
|
||||
return ToolResult.Fail("'subreddit' is required (without r/).");
|
||||
|
||||
var url = $"https://www.reddit.com/r/{Uri.EscapeDataString(subreddit)}/new.json?limit={limit}";
|
||||
|
||||
@@ -209,7 +209,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return ToolResult.Fail($"Reddit API Fehler: {response.StatusCode} - {content}");
|
||||
return ToolResult.Fail($"Reddit API error: {response.StatusCode} - {content}");
|
||||
|
||||
// Kompakte Darstellung: Nur relevante Felder extrahieren
|
||||
var posts = ExtractRedditPosts(content);
|
||||
@@ -219,7 +219,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
private async Task<ToolResult> HandleYoutubeTranscriptAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var url = input.TryGetProperty("url", out var u) ? u.GetString() : "";
|
||||
if (string.IsNullOrWhiteSpace(url)) return ToolResult.Fail("URL fehlt.");
|
||||
if (string.IsNullOrWhiteSpace(url)) return ToolResult.Fail("URL missing.");
|
||||
|
||||
var (available, path, hasFfmpeg, ffmpegPath) = await VerifyYtDlpAsync();
|
||||
if (!available) return ToolResult.Fail(GetYtDlpInstallationGuide());
|
||||
@@ -233,10 +233,10 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
// Wir starten den Prozess im Hintergrund
|
||||
_ = ProcessYoutubeVideoAsync(url, context.ToolConfig, context.WorkspacePath!, context.Logger, path!, hasFfmpeg, ffmpegPath, ct);
|
||||
|
||||
return ToolResult.Ok($"Transkription für {url} wurde gestartet und läuft im Hintergrund. " +
|
||||
"Das Herunterladen, Aufteilen und Transkribieren kann je nach Videolänge 2–10 Minuten dauern. " +
|
||||
"Du wirst automatisch benachrichtigt sobald das Transkript fertig ist (Transcript Notifier). " +
|
||||
"Antworte dem Benutzer, dass die Transkription gestartet wurde und er in wenigen Minuten das Ergebnis erhält.");
|
||||
return ToolResult.Ok($"Transcription for {url} has been started and is running in the background. " +
|
||||
"Downloading, splitting, and transcribing may take 2–10 minutes depending on video length. " +
|
||||
"You will be automatically notified once the transcript is ready (Transcript Notifier). " +
|
||||
"Tell the user that transcription has been started and they will receive the result in a few minutes.");
|
||||
}
|
||||
|
||||
private async Task SavePendingTaskAsync(AgentToolContext context, string url, CancellationToken ct)
|
||||
@@ -280,21 +280,21 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
public IReadOnlyList<ToolJobDefinition> GetJobDefinitions() =>
|
||||
[
|
||||
new("sm_yt_monitor", "YouTube Monitor", "Prüft Kanäle auf neue Videos und startet Transkription"),
|
||||
new("sm_transcript_notifier", "Transcript Notifier", "Informiert den Agenten über neue Transkripte"),
|
||||
new("sm_x_account_monitor", "X Account Monitor", "Überwacht konfigurierte X-Accounts auf neue Tweets (Config: xWatchAccounts)"),
|
||||
new("sm_reddit_monitor", "Reddit Monitor", "Überwacht konfigurierte Subreddits auf neue Posts (Config: redditWatchSubreddits)")
|
||||
new("sm_yt_monitor", "YouTube Monitor", "Checks channels for new videos and starts transcription"),
|
||||
new("sm_transcript_notifier", "Transcript Notifier", "Informs the agent about new transcripts"),
|
||||
new("sm_x_account_monitor", "X Account Monitor", "Monitors configured X accounts for new tweets (Config: xWatchAccounts)"),
|
||||
new("sm_reddit_monitor", "Reddit Monitor", "Monitors configured subreddits for new posts (Config: redditWatchSubreddits)")
|
||||
];
|
||||
|
||||
public async Task<ToolJobResult> ExecuteJobAsync(string jobTypeId, IReadOnlyDictionary<string, object?> toolConfig, IStateStore stateStore, ILogger logger, CancellationToken ct, string? agentId = null, string? workspacePath = null)
|
||||
{
|
||||
// Direkt übergebenen Pfad nutzen, Fallback auf per-agent scoped StateStore
|
||||
// Use directly passed path, fallback to per-agent scoped StateStore
|
||||
if (string.IsNullOrWhiteSpace(workspacePath) && !string.IsNullOrWhiteSpace(agentId))
|
||||
workspacePath = await stateStore.GetAsync($"sm_workspace_path:{agentId}", ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(workspacePath))
|
||||
{
|
||||
return ToolJobResult.NoAction("WorkspacePath unbekannt. Bitte Tool einmal manuell ausführen.");
|
||||
return ToolJobResult.NoAction("WorkspacePath unknown. Please run tool manually once.");
|
||||
}
|
||||
|
||||
return jobTypeId switch
|
||||
@@ -303,17 +303,17 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
"sm_transcript_notifier" => await ExecuteTranscriptNotifierJobAsync(toolConfig, workspacePath, stateStore, logger, ct),
|
||||
"sm_x_account_monitor" => await ExecuteXAccountMonitorJobAsync(toolConfig, workspacePath, stateStore, logger, ct),
|
||||
"sm_reddit_monitor" => await ExecuteRedditMonitorJobAsync(toolConfig, workspacePath, stateStore, logger, ct),
|
||||
_ => ToolJobResult.NoAction($"Unbekannter Job: {jobTypeId}")
|
||||
_ => ToolJobResult.NoAction($"Unknown job: {jobTypeId}")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ToolJobResult> ExecuteYoutubeMonitorJobAsync(IReadOnlyDictionary<string, object?> toolConfig, string workspacePath, IStateStore stateStore, ILogger logger, CancellationToken ct)
|
||||
{
|
||||
var (available, path, hasFfmpeg, ffmpegPath) = await VerifyYtDlpAsync();
|
||||
if (!available) return ToolJobResult.NoAction("YouTube Monitor deaktiviert: " + GetYtDlpInstallationGuide());
|
||||
if (!available) return ToolJobResult.NoAction("YouTube Monitor disabled: " + GetYtDlpInstallationGuide());
|
||||
|
||||
var channels = GetConfigArray(toolConfig, "youtubeChannels").ToList();
|
||||
if (channels.Count == 0) return ToolJobResult.NoAction("Keine Kanäle konfiguriert.");
|
||||
if (channels.Count == 0) return ToolJobResult.NoAction("No channels configured.");
|
||||
|
||||
var lastVidsJson = await stateStore.GetAsync("sm_yt_last_vids", ct) ?? "{}";
|
||||
var lastVids = JsonSerializer.Deserialize<Dictionary<string, string>>(lastVidsJson) ?? new();
|
||||
@@ -324,16 +324,16 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
if (channelInput == null) continue;
|
||||
try
|
||||
{
|
||||
// Normalisierter Key für State-Store (ohne URL-Variationen)
|
||||
// Normalized key for state store
|
||||
var stateKey = channelInput.Trim().TrimStart('@').ToLowerInvariant();
|
||||
var latestId = await GetLatestVideoIdAsync(channelInput, path!, ct);
|
||||
if (string.IsNullOrWhiteSpace(latestId)) continue;
|
||||
|
||||
if (!lastVids.TryGetValue(stateKey, out var lastId) || lastId != latestId)
|
||||
{
|
||||
logger.LogInformation("Neues Video gefunden: {VideoId} auf {Channel}", latestId, channelInput);
|
||||
logger.LogInformation("New video found: {VideoId} on {Channel}", latestId, channelInput);
|
||||
|
||||
// Transkription starten
|
||||
// Start transcription
|
||||
_ = ProcessYoutubeVideoAsync($"https://www.youtube.com/watch?v={latestId}", toolConfig, workspacePath, logger, path!, hasFfmpeg, ffmpegPath, ct);
|
||||
|
||||
lastVids[stateKey] = latestId;
|
||||
@@ -342,20 +342,20 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning("Fehler beim Prüfen von Kanal {Channel}: {Msg}", channelInput, ex.Message);
|
||||
logger.LogWarning("Error checking channel {Channel}: {Msg}", channelInput, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
await stateStore.SetAsync("sm_yt_last_vids", JsonSerializer.Serialize(lastVids), ct);
|
||||
|
||||
return ToolJobResult.NoAction(count > 0 ? $"{count} neue Videos zur Transkription gestartet." : "Keine neuen Videos.");
|
||||
return ToolJobResult.NoAction(count > 0 ? $"{count} new video(s) started for transcription." : "No new videos.");
|
||||
}
|
||||
|
||||
private async Task<ToolJobResult> ExecuteTranscriptNotifierJobAsync(IReadOnlyDictionary<string, object?> toolConfig, string workspacePath, IStateStore stateStore, ILogger logger, CancellationToken ct)
|
||||
{
|
||||
var targetDir = Path.Combine(workspacePath, "YTTranscript");
|
||||
if (!Directory.Exists(targetDir))
|
||||
return ToolJobResult.NoAction("YTTranscript-Verzeichnis existiert noch nicht");
|
||||
return ToolJobResult.NoAction("YTTranscript directory does not exist yet");
|
||||
|
||||
var processedFilesJson = await stateStore.GetAsync("sm_processed_files", ct) ?? "[]";
|
||||
var processedFiles = JsonSerializer.Deserialize<HashSet<string>>(processedFilesJson) ?? new();
|
||||
@@ -369,7 +369,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
.ToList();
|
||||
|
||||
if (newFiles.Count == 0)
|
||||
return ToolJobResult.NoAction($"Keine neuen Transkripte ({allTxtFiles.Length} vorhanden, {processedFiles.Count} bereits verarbeitet)");
|
||||
return ToolJobResult.NoAction($"No new transcripts ({allTxtFiles.Length} existing, {processedFiles.Count} already processed)");
|
||||
|
||||
foreach (var f in newFiles) if (f != null) processedFiles.Add(f);
|
||||
await stateStore.SetAsync("sm_processed_files", JsonSerializer.Serialize(processedFiles), ct);
|
||||
@@ -386,11 +386,11 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
var openTasks = tasks?.Where(t => !t.Completed).ToList();
|
||||
if (openTasks?.Count > 0)
|
||||
{
|
||||
pendingContext = "\n\n[AUFTRAGSKONTEXT - Wer wartet auf diese Transkripte:]\n" +
|
||||
pendingContext = "\n\n[TASK CONTEXT - Who is waiting for these transcripts:]\n" +
|
||||
string.Join("\n", openTasks.Select(t =>
|
||||
$" - Video-URL: {t.Url} | Auftraggeber-Agent: {t.RequestedBy} | Angefragt: {t.RequestedAt:yyyy-MM-dd HH:mm}"));
|
||||
pendingContext += "\n\nWICHTIG: Informiere den Auftraggeber-Agenten per AgentComm über das fertige Transkript! " +
|
||||
"Fasse den Inhalt kurz zusammen und teile ihm mit wo er das vollständige Transkript lesen kann.";
|
||||
$" - Video URL: {t.Url} | Requester Agent: {t.RequestedBy} | Requested: {t.RequestedAt:yyyy-MM-dd HH:mm}"));
|
||||
pendingContext += "\n\nIMPORTANT: Inform the requester agent via AgentComm about the completed transcript! " +
|
||||
"Briefly summarize the content and let them know where to read the full transcript.";
|
||||
|
||||
// Tasks als completed markieren
|
||||
foreach (var t in openTasks) t.Completed = true;
|
||||
@@ -399,17 +399,17 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Konnte pending_tasks.json nicht lesen");
|
||||
logger.LogWarning(ex, "Could not read pending_tasks.json");
|
||||
}
|
||||
}
|
||||
|
||||
return ToolJobResult.WakeStateless(
|
||||
$"[SYSTEM-EVENT: Neue Transkripte verfügbar]\n" +
|
||||
$"Es liegen {newFiles.Count} neue Video-Transkripte in deinem persönlichen Workspace unter YTTranscript/ bereit:\n" +
|
||||
$"[SYSTEM EVENT: New transcripts available]\n" +
|
||||
$"There are {newFiles.Count} new video transcript(s) ready in your personal workspace under YTTranscript/:\n" +
|
||||
string.Join("\n", newFiles.Select(f => $" - {f}")) +
|
||||
pendingContext +
|
||||
"\n\nBitte lies die Transkripte jetzt mit dem FileRW-Tool (action: read, workspace: personal, path: YTTranscript/<dateiname>) und verarbeite sie.",
|
||||
logSummary: $"{newFiles.Count} neue Transkripte gefunden"
|
||||
"\n\nPlease read the transcripts now using the FileRW tool (action: read, workspace: personal, path: YTTranscript/<filename>) and process them.",
|
||||
logSummary: $"{newFiles.Count} new transcript(s) found"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -452,9 +452,9 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
try
|
||||
{
|
||||
// Eingabe validieren, bevor irgendetwas an einen externen Prozess geht.
|
||||
// Validate input before passing to external process
|
||||
if (!YouTubeUrl.TryResolveChannelUrl(url, out var safeUrl, out var urlError))
|
||||
throw new ArgumentException($"Ungültige YouTube-Adresse: {urlError}");
|
||||
throw new ArgumentException($"Invalid YouTube URL: {urlError}");
|
||||
|
||||
var targetDir = Path.Combine(workspacePath, "YTTranscript");
|
||||
var tmpDir = Path.Combine(targetDir, "tmp");
|
||||
@@ -472,7 +472,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
if (hasFfmpeg)
|
||||
{
|
||||
// Mit ffmpeg: yt-dlp konvertiert direkt zu mp3
|
||||
// With ffmpeg: yt-dlp converts directly to mp3
|
||||
audioFile = Path.Combine(tmpDir, $"{videoId}.mp3");
|
||||
await RunYtDlpAsync(ytDlpPath,
|
||||
YouTubeUrl.BuildAudioDownloadArgs(audioFile, safeUrl, ffmpegDir, convertToMp3: true),
|
||||
@@ -480,15 +480,14 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ohne ffmpeg: Audio im Originalformat herunterladen
|
||||
// Without ffmpeg: download audio in original format
|
||||
var outputTemplate = Path.Combine(tmpDir, $"{videoId}.%(ext)s");
|
||||
await RunYtDlpAsync(ytDlpPath,
|
||||
YouTubeUrl.BuildAudioDownloadArgs(outputTemplate, safeUrl, null, convertToMp3: false),
|
||||
logger, ct);
|
||||
audioFile = ""; // wird unten gesucht
|
||||
audioFile = ""; // looked up below
|
||||
}
|
||||
|
||||
// Datei finden (bei fehlendem ffmpeg hat die Datei eine unbekannte Extension)
|
||||
if (!File.Exists(audioFile))
|
||||
{
|
||||
var foundFile = Directory.GetFiles(tmpDir, videoId + ".*")
|
||||
@@ -497,12 +496,12 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
if (foundFile != null)
|
||||
{
|
||||
audioFile = foundFile;
|
||||
logger.LogInformation("Audiodatei gefunden: {File} ({Size} MB)",
|
||||
logger.LogInformation("Audio file found: {File} ({Size} MB)",
|
||||
Path.GetFileName(foundFile), new FileInfo(foundFile).Length / 1024.0 / 1024.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FileNotFoundException("Audio konnte nicht heruntergeladen werden.");
|
||||
throw new FileNotFoundException("Could not download audio.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,34 +511,34 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
logger.LogError("OpenRouter API Key fehlt in SocialMediaManager Tool-Config (openRouterApiKey). " +
|
||||
"Audio wurde heruntergeladen nach: {AudioFile}. Bitte openRouterApiKey in der Agent-Konfiguration setzen.", audioFile);
|
||||
logger.LogError("OpenRouter API key missing in SocialMediaManager tool config (openRouterApiKey). " +
|
||||
"Audio was downloaded to: {AudioFile}. Please configure openRouterApiKey in agent configuration.", audioFile);
|
||||
return audioFile;
|
||||
}
|
||||
|
||||
var fileSize = new FileInfo(audioFile).Length;
|
||||
logger.LogInformation("Starte Transkription für {File} ({Size:F1} MB) via OpenRouter ({Model}), ffmpeg={HasFfmpeg}",
|
||||
logger.LogInformation("Starting transcription for {File} ({Size:F1} MB) via OpenRouter ({Model}), ffmpeg={HasFfmpeg}",
|
||||
Path.GetFileName(audioFile), fileSize / 1024.0 / 1024.0, model, hasFfmpeg);
|
||||
|
||||
var transcript = await TranscribeAudioAsync(audioFile, apiKey, model, hasFfmpeg, ffmpegPath, ytDlpPath, logger, ct);
|
||||
|
||||
// 4. Transkript speichern
|
||||
// 4. Save transcript
|
||||
var transcriptFile = Path.Combine(targetDir, $"{videoId}.txt");
|
||||
await File.WriteAllTextAsync(transcriptFile, transcript, ct);
|
||||
logger.LogInformation("Transkript erfolgreich gespeichert: {File}", Path.GetFileName(transcriptFile));
|
||||
logger.LogInformation("Transcript successfully saved: {File}", Path.GetFileName(transcriptFile));
|
||||
|
||||
return transcriptFile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fehler im ProcessYoutubeVideoAsync für {Url}", url);
|
||||
logger.LogError(ex, "Error in ProcessYoutubeVideoAsync for {Url}", url);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Startet yt-dlp. Die Argumente werden einzeln übergeben (ArgumentList), damit
|
||||
/// kein Wert versehentlich als weitere Option interpretiert werden kann.
|
||||
/// Starts yt-dlp. Arguments are passed individually (ArgumentList) so that
|
||||
/// no value can accidentally be interpreted as another option.
|
||||
/// </summary>
|
||||
private async Task RunYtDlpAsync(string ytDlpPath, List<string> arguments, ILogger logger, CancellationToken ct)
|
||||
{
|
||||
@@ -565,7 +564,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fehler beim Aufruf von yt-dlp.");
|
||||
logger.LogError(ex, "Error calling yt-dlp.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -577,13 +576,13 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
if (!fileInfo.Exists)
|
||||
throw new FileNotFoundException($"Audiodatei nicht gefunden: {filePath}");
|
||||
throw new FileNotFoundException($"Audio file not found: {filePath}");
|
||||
|
||||
// Kleine Dateien direkt transkribieren
|
||||
if (fileInfo.Length <= MaxChunkBytes)
|
||||
return await TranscribeChunkAsync(filePath, apiKey, model, logger, ct);
|
||||
|
||||
logger.LogInformation("Datei ist {Size:F1} MB — Chunking erforderlich", fileInfo.Length / 1024.0 / 1024.0);
|
||||
logger.LogInformation("File is {Size:F1} MB — chunking required", fileInfo.Length / 1024.0 / 1024.0);
|
||||
|
||||
// Große Dateien in Chunks aufteilen
|
||||
var chunkDir = Path.Combine(Path.GetDirectoryName(filePath)!, "chunks");
|
||||
@@ -596,7 +595,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
if (hasFfmpeg)
|
||||
{
|
||||
// ffmpeg: 20-Minuten-Segmente als mp3 (unter 25 MB Whisper-Limit bei q:a 5)
|
||||
// ffmpeg: 20-minute segments as mp3 (under 25 MB Whisper limit at q:a 5)
|
||||
var chunkPattern = Path.Combine(chunkDir, "chunk_%03d.mp3");
|
||||
var ffmpegExe = !string.IsNullOrEmpty(ffmpegPath) ? ffmpegPath : "ffmpeg";
|
||||
var psi = new ProcessStartInfo(ffmpegExe)
|
||||
@@ -629,22 +628,22 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback ohne ffmpeg: yt-dlp --download-sections für zeitbasierte Segmente
|
||||
logger.LogInformation("ffmpeg nicht verfügbar — verwende yt-dlp Segmentierung");
|
||||
// Fallback without ffmpeg
|
||||
logger.LogInformation("ffmpeg not available — using yt-dlp segmentation");
|
||||
chunks = await ChunkWithYtDlpAsync(filePath, chunkDir, ytDlpPath, logger, ct);
|
||||
}
|
||||
|
||||
if (chunks.Count == 0)
|
||||
throw new InvalidOperationException(
|
||||
"Chunking fehlgeschlagen — keine Segmente erzeugt. " +
|
||||
(hasFfmpeg ? "ffmpeg-Fehler?" : "Installiere ffmpeg für zuverlässiges Chunking: 'winget install ffmpeg'"));
|
||||
"Chunking failed — no segments produced. " +
|
||||
(hasFfmpeg ? "ffmpeg error?" : "Install ffmpeg for reliable chunking: 'winget install ffmpeg'"));
|
||||
|
||||
logger.LogInformation("{Count} Chunks erzeugt, starte Transkription...", chunks.Count);
|
||||
logger.LogInformation("{Count} chunks produced, starting transcription...", chunks.Count);
|
||||
|
||||
var transcripts = new List<string>();
|
||||
for (int i = 0; i < chunks.Count; i++)
|
||||
{
|
||||
logger.LogInformation("Transkribiere Chunk {Index}/{Total}: {File} ({Size:F1} MB)",
|
||||
logger.LogInformation("Transcribing chunk {Index}/{Total}: {File} ({Size:F1} MB)",
|
||||
i + 1, chunks.Count, Path.GetFileName(chunks[i]), new FileInfo(chunks[i]).Length / 1024.0 / 1024.0);
|
||||
var chunkText = await TranscribeChunkAsync(chunks[i], apiKey, model, logger, ct);
|
||||
transcripts.Add(chunkText);
|
||||
@@ -660,36 +659,31 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fallback-Chunking ohne ffmpeg: Konvertiert die gesamte Audiodatei mit yt-dlp
|
||||
/// in ein komprimiertes Format (Opus/niedrige Bitrate), um unter das Größenlimit zu kommen.
|
||||
/// Falls die Datei dann immer noch zu groß ist, wird ein Fehler geworfen.
|
||||
/// Fallback chunking without ffmpeg.
|
||||
///
|
||||
/// <para>yt-dlp cannot split a file that is already downloaded, and without ffmpeg
|
||||
/// nothing else here can either. So a file that fits the Whisper limit is sent as a
|
||||
/// single chunk; anything larger fails with an installation hint instead of a
|
||||
/// truncated transcript.</para>
|
||||
/// </summary>
|
||||
private async Task<List<string>> ChunkWithYtDlpAsync(string filePath, string chunkDir,
|
||||
string ytDlpPath, ILogger logger, CancellationToken ct)
|
||||
{
|
||||
// Strategie: yt-dlp kann ohne ffmpeg nicht segmentieren.
|
||||
// Wir versuchen stattdessen, die Datei einfach mit reduzierter Qualität neu zu verarbeiten.
|
||||
// Da die Originaldatei schon heruntergeladen ist und yt-dlp nicht direkt lokale Dateien
|
||||
// konvertieren kann, müssen wir einen anderen Weg gehen.
|
||||
|
||||
// Prüfe ob die Datei vielleicht doch knapp genug ist (mit Toleranz)
|
||||
var fileSize = new FileInfo(filePath).Length;
|
||||
if (fileSize <= 25 * 1024 * 1024) // Whisper-Limit exakt 25 MB
|
||||
if (fileSize <= 25 * 1024 * 1024) // Whisper limit exact 25 MB
|
||||
{
|
||||
logger.LogInformation("Datei ist {Size:F1} MB — versuche trotzdem Direkt-Upload (Whisper-Limit: 25 MB)", fileSize / 1024.0 / 1024.0);
|
||||
logger.LogInformation("File is {Size:F1} MB — attempting direct upload anyway (Whisper limit: 25 MB)", fileSize / 1024.0 / 1024.0);
|
||||
return [filePath];
|
||||
}
|
||||
|
||||
// Ohne ffmpeg können wir die Datei nicht lokal aufteilen.
|
||||
// Letzter Versuch: Datei als einzelnen Chunk senden und auf den API-Fehler reagieren.
|
||||
logger.LogWarning(
|
||||
"Audiodatei ist {Size:F1} MB und ffmpeg ist nicht installiert. " +
|
||||
"Chunking ohne ffmpeg nicht möglich. Bitte ffmpeg installieren: 'winget install ffmpeg'",
|
||||
"Audio file is {Size:F1} MB and ffmpeg is not installed. " +
|
||||
"Chunking without ffmpeg not possible. Please install ffmpeg: 'winget install ffmpeg'",
|
||||
fileSize / 1024.0 / 1024.0);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Die Audiodatei ist {fileSize / 1024.0 / 1024.0:F1} MB groß und überschreitet das Whisper-Limit von 25 MB. " +
|
||||
"Für große Videos wird ffmpeg zum Aufteilen benötigt. Installation: 'winget install ffmpeg'");
|
||||
$"The audio file is {fileSize / 1024.0 / 1024.0:F1} MB and exceeds the Whisper limit of 25 MB. " +
|
||||
"For large videos, ffmpeg is required for splitting. Installation: 'winget install ffmpeg'");
|
||||
}
|
||||
|
||||
private static string GetMimeType(string filePath) => Path.GetExtension(filePath).ToLowerInvariant() switch
|
||||
@@ -732,9 +726,9 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogError("STT Fehler für {File}: {Status} - {Response}",
|
||||
logger.LogError("STT error for {File}: {Status} - {Response}",
|
||||
fileName, response.StatusCode, responseContent);
|
||||
throw new Exception($"STT Fehler ({fileName}): {response.StatusCode} - {responseContent}");
|
||||
throw new Exception($"STT error ({fileName}): {response.StatusCode} - {responseContent}");
|
||||
}
|
||||
|
||||
var doc = JsonDocument.Parse(responseContent);
|
||||
@@ -858,24 +852,23 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
private string GetYtDlpInstallationGuide()
|
||||
{
|
||||
return "FEHLER: Abhängigkeiten fehlen für YouTube-Funktionen.\n\n" +
|
||||
"1. yt-dlp: Lade 'yt-dlp.exe' von https://github.com/yt-dlp/yt-dlp/releases herunter und lege sie in 'tools/yt-dlp.exe' ab.\n" +
|
||||
"2. ffmpeg (optional): Empfohlen für MP3-Konvertierung. Ohne ffmpeg werden WebM/M4A-Dateien genutzt.\n" +
|
||||
return "ERROR: Dependencies missing for YouTube features.\n\n" +
|
||||
"1. yt-dlp: Download 'yt-dlp.exe' from https://github.com/yt-dlp/yt-dlp/releases and place it in 'tools/yt-dlp.exe'.\n" +
|
||||
"2. ffmpeg (optional): Recommended for MP3 conversion. Without ffmpeg, WebM/M4A files will be used.\n" +
|
||||
" Installation via: 'winget install ffmpeg'.\n\n" +
|
||||
"Tipp: Nutze 'winget install yt-dlp ffmpeg' für eine schnelle Installation beider Tools.";
|
||||
"Tip: Use 'winget install yt-dlp ffmpeg' for a quick installation of both tools.";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// X/TWITTER MONITOR JOB
|
||||
//
|
||||
// Cost optimisation:
|
||||
// - query ALL watched accounts in ONE search query
|
||||
// ("from:user1 OR from:user2 OR from:user3") → 1 API call instead of N
|
||||
// - since_id tracking → only tweets newer than the last check
|
||||
// - results cached locally → manual queries cost no API call
|
||||
// - first run: set since_id to "now" without fetching old tweets (zero cost)
|
||||
// ═══════════════════════════════════════════════════
|
||||
//
|
||||
// Kostenoptimierung:
|
||||
// - ALLE überwachten Accounts in EINER einzigen Search-Query abfragen
|
||||
// ("from:user1 OR from:user2 OR from:user3") → 1 API-Call statt N
|
||||
// - since_id tracking → nur neue Tweets seit letzter Prüfung
|
||||
// - Ergebnisse lokal cachen → manuelle Abfragen kosten keinen API-Call
|
||||
// - Erster Lauf: since_id auf "jetzt" setzen ohne alte Tweets abzurufen (0 Kosten)
|
||||
//
|
||||
|
||||
private async Task<ToolJobResult> ExecuteXAccountMonitorJobAsync(
|
||||
IReadOnlyDictionary<string, object?> toolConfig, string workspacePath,
|
||||
@@ -883,11 +876,11 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
var apiKey = toolConfig.GetValueOrDefault("xApiKey")?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
return ToolJobResult.NoAction("X API Key (xApiKey) nicht konfiguriert");
|
||||
return ToolJobResult.NoAction("X API key (xApiKey) not configured");
|
||||
|
||||
var accounts = GetConfigArray(toolConfig, "xWatchAccounts").ToList();
|
||||
if (accounts.Count == 0)
|
||||
return ToolJobResult.NoAction("Keine X-Accounts konfiguriert (xWatchAccounts)");
|
||||
return ToolJobResult.NoAction("No X accounts configured (xWatchAccounts)");
|
||||
|
||||
var cleanAccounts = accounts
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
@@ -926,11 +919,11 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
if (!string.IsNullOrWhiteSpace(sinceId))
|
||||
{
|
||||
await stateStore.SetAsync("sm_x_global_since_id", sinceId, ct);
|
||||
logger.LogInformation("X Monitor: Baseline gesetzt (since_id={SinceId}). Kosten: 1 API-Call. Ab jetzt nur noch neue Tweets.", sinceId);
|
||||
return ToolJobResult.NoAction($"Baseline gesetzt. Ab nächstem Tick werden nur neue Tweets gemeldet. (1 API-Call)");
|
||||
logger.LogInformation("X Monitor: Baseline set (since_id={SinceId}). Cost: 1 API call. From now on only new tweets will be reported.", sinceId);
|
||||
return ToolJobResult.NoAction($"Baseline set. Starting with next tick only new tweets will be reported. (1 API call)");
|
||||
}
|
||||
|
||||
return ToolJobResult.NoAction("Konnte Baseline nicht setzen — nächster Versuch beim nächsten Tick.");
|
||||
return ToolJobResult.NoAction("Could not set baseline — retry on next tick.");
|
||||
}
|
||||
|
||||
// ─── Hauptlogik: 1 Search-Query für ALLE Accounts ───
|
||||
@@ -943,7 +936,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
if (query.Length > 512)
|
||||
{
|
||||
// Bei sehr vielen Accounts: aufteilen in Batches
|
||||
logger.LogWarning("X Monitor: Query zu lang ({Len} Zeichen), verwende nur die ersten Accounts", query.Length);
|
||||
logger.LogWarning("X Monitor: Query too long ({Len} characters), using only the first accounts", query.Length);
|
||||
var trimmedAccounts = new List<string>();
|
||||
var currentLen = 0;
|
||||
foreach (var acc in cleanAccounts)
|
||||
@@ -972,8 +965,8 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogWarning("X Monitor API Fehler: {Status} - {Content}", response.StatusCode, content);
|
||||
return ToolJobResult.NoAction($"X API Fehler: {response.StatusCode}");
|
||||
logger.LogWarning("X Monitor API error: {Status} - {Content}", response.StatusCode, content);
|
||||
return ToolJobResult.NoAction($"X API error: {response.StatusCode}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
@@ -992,7 +985,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
dataArr.ValueKind != JsonValueKind.Array ||
|
||||
dataArr.GetArrayLength() == 0)
|
||||
{
|
||||
return ToolJobResult.NoAction("Keine neuen Tweets (1 API-Call)");
|
||||
return ToolJobResult.NoAction("No new tweets (1 API call)");
|
||||
}
|
||||
|
||||
// Author-ID → Username Mapping aus "includes.users" aufbauen
|
||||
@@ -1026,7 +1019,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogInformation("X Monitor: {Count} neue Tweets gefunden (1 API-Call für {AccountCount} Accounts)",
|
||||
logger.LogInformation("X Monitor: {Count} new tweets found (1 API call for {AccountCount} accounts)",
|
||||
allNewTweets.Count, cleanAccounts.Count);
|
||||
|
||||
// Ergebnisse im Workspace cachen
|
||||
@@ -1049,13 +1042,13 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
return $" - {account}: {preview}";
|
||||
}));
|
||||
|
||||
var extra = allNewTweets.Count > 10 ? $"\n ... und {allNewTweets.Count - 10} weitere" : "";
|
||||
var extra = allNewTweets.Count > 10 ? $"\n ... and {allNewTweets.Count - 10} more" : "";
|
||||
|
||||
return ToolJobResult.Wake(
|
||||
$"[X Monitor] {allNewTweets.Count} neue Tweets (1 API-Call für {cleanAccounts.Count} Accounts):\n\n{summary}{extra}\n\n" +
|
||||
$"Vollständige Daten gespeichert in: personal:/XMonitor/{Path.GetFileName(resultFile)}\n" +
|
||||
$"Lies die Datei mit FileRW und analysiere die relevanten Tweets.",
|
||||
logSummary: $"X: {allNewTweets.Count} neue Tweets (1 Call)"
|
||||
$"[X Monitor] {allNewTweets.Count} new tweet(s) (1 API call for {cleanAccounts.Count} accounts):\n\n{summary}{extra}\n\n" +
|
||||
$"Full data saved in: personal:/XMonitor/{Path.GetFileName(resultFile)}\n" +
|
||||
$"Read the file with FileRW and analyze the relevant tweets.",
|
||||
logSummary: $"X: {allNewTweets.Count} new tweet(s) (1 call)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1069,7 +1062,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
var subreddits = GetConfigArray(toolConfig, "redditWatchSubreddits").ToList();
|
||||
if (subreddits.Count == 0)
|
||||
return ToolJobResult.NoAction("Keine Subreddits konfiguriert (redditWatchSubreddits)");
|
||||
return ToolJobResult.NoAction("No subreddits configured (redditWatchSubreddits)");
|
||||
|
||||
var redditLimit = 15; // Posts pro Subreddit
|
||||
if (toolConfig.TryGetValue("redditPostLimit", out var rl) && rl is JsonElement rlElem)
|
||||
@@ -1133,12 +1126,12 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
allNewPosts.Add(post);
|
||||
}
|
||||
|
||||
logger.LogInformation("Reddit Monitor: {Count} neue Posts in r/{Sub}", newPosts.Count, cleanSub);
|
||||
logger.LogInformation("Reddit Monitor: {Count} new posts in r/{Sub}", newPosts.Count, cleanSub);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"r/{cleanSub}: {ex.Message}");
|
||||
logger.LogWarning(ex, "Reddit Monitor Fehler für r/{Sub}", cleanSub);
|
||||
logger.LogWarning(ex, "Reddit Monitor error for r/{Sub}", cleanSub);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,11 +1140,11 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
if (allNewPosts.Count == 0)
|
||||
{
|
||||
var errMsg = errors.Count > 0 ? $" (Fehler: {string.Join("; ", errors)})" : "";
|
||||
return ToolJobResult.NoAction($"Keine neuen Reddit-Posts{errMsg}");
|
||||
var errMsg = errors.Count > 0 ? $" (Errors: {string.Join("; ", errors)})" : "";
|
||||
return ToolJobResult.NoAction($"No new Reddit posts{errMsg}");
|
||||
}
|
||||
|
||||
// Ergebnisse im Workspace speichern
|
||||
// Save results in workspace
|
||||
var resultDir = Path.Combine(workspacePath, "RedditMonitor");
|
||||
Directory.CreateDirectory(resultDir);
|
||||
var resultFile = Path.Combine(resultDir, $"{DateTime.Now:yyyy-MM-dd_HHmmss}_posts.json");
|
||||
@@ -1167,17 +1160,17 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
return $" - [{sub}] {preview}";
|
||||
}));
|
||||
|
||||
var extra = allNewPosts.Count > 10 ? $"\n ... und {allNewPosts.Count - 10} weitere" : "";
|
||||
var errorInfo = errors.Count > 0 ? $"\n\n⚠ Fehler bei: {string.Join(", ", errors)}" : "";
|
||||
var extra = allNewPosts.Count > 10 ? $"\n ... and {allNewPosts.Count - 10} more" : "";
|
||||
var errorInfo = errors.Count > 0 ? $"\n\n⚠ Errors with: {string.Join(", ", errors)}" : "";
|
||||
|
||||
// Alte Cache-Dateien aufräumen
|
||||
// Cleanup old cache files
|
||||
CleanupOldFiles(resultDir, "*.json", keepCount: 20);
|
||||
|
||||
return ToolJobResult.Wake(
|
||||
$"[Reddit Monitor] {allNewPosts.Count} neue Posts aus überwachten Subreddits:\n\n{summary}{extra}\n\n" +
|
||||
$"Vollständige Daten gespeichert in: personal:/RedditMonitor/{Path.GetFileName(resultFile)}\n" +
|
||||
$"Lies die Datei mit FileRW und analysiere die relevanten Posts.{errorInfo}",
|
||||
logSummary: $"Reddit Monitor: {allNewPosts.Count} neue Posts"
|
||||
$"[Reddit Monitor] {allNewPosts.Count} new post(s) from monitored subreddits:\n\n{summary}{extra}\n\n" +
|
||||
$"Full data saved in: personal:/RedditMonitor/{Path.GetFileName(resultFile)}\n" +
|
||||
$"Read the file with FileRW and analyze the relevant posts.{errorInfo}",
|
||||
logSummary: $"Reddit Monitor: {allNewPosts.Count} new post(s)"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user