using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; using Nexus.Api.Models; namespace Nexus.Api.Services; public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration configuration) { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private string? GetPassword() { var password = configuration["Integrations:OpenClaw:Password"]; if (string.IsNullOrWhiteSpace(password)) password = configuration["Integrations:OpenClaw:Token"]; return string.IsNullOrWhiteSpace(password) ? null : password; } private void ApplyAuth(HttpRequestMessage request) { var password = GetPassword(); if (password is not null) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", password); } public async Task InvokeToolAsync(string tool, object? args = null) { try { using var request = new HttpRequestMessage(HttpMethod.Post, "/tools/invoke"); ApplyAuth(request); var body = new Dictionary { ["tool"] = tool }; if (args is not null) body["args"] = args; request.Content = JsonContent.Create(body); using var response = await httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); if (string.IsNullOrWhiteSpace(json)) return null; var node = JsonNode.Parse(json); if (node?["ok"]?.GetValue() == true && node["result"] is not null) return node["result"]; return node; } catch { return null; } } public async Task> GetAgentsAsync() { var agentDefs = new[] { new { Id = "iris", Name = "Chief of Staff", Model = "deepseek/deepseek-v4-flash", Description = "Zentrale operative Führungsinstanz. Strukturiert Aufgaben, bewertet Risiken, steuert spezialisierte Agenten und eskaliert kritische Entscheidungen.", Tags = new[] { "Orchestration", "Delegation", "Approval", "Risk Management" } }, new { Id = "programmer", Name = "Full-Stack Developer", Model = "deepseek/deepseek-v4-flash", Description = "Primärer Entwicklungsagent. Implementiert Features, behebt Bugs und schreibt Code im gesamten Stack — autonom im Rahmen seines Scopes.", Tags = new[] { "Full-Stack", "TypeScript", "C#", "Vue", ".NET", "Builds" } }, new { Id = "reviewer", Name = "Code Quality Assurance", Model = "deepseek/deepseek-v4-pro", Description = "Code-Qualitätskontrolle. Prüft Diffs auf Bugs, Regressionen, Sicherheitslücken und Wartbarkeit. Berichtet Findings strukturiert und knapp.", Tags = new[] { "Code Review", "Testing", "Security", "Quality" } }, new { Id = "architekt", Name = "Infrastructure Architect", Model = "deepseek/deepseek-v4-pro", Description = "Verwaltet die gesamte Server-Infrastruktur. Deployt Services, konfiguriert Docker, Nginx und Firewall. Stellt sicher, dass die Produktivumgebung stabil und sicher läuft.", Tags = new[] { "Docker", "Nginx", "CI/CD", "Firewall", "VPS" } }, new { Id = "executor", Name = "Host Executor", Model = "deepseek/deepseek-v4-flash", Description = "Einziger Agent mit Host-Exec-Rechten. Führt Docker- und Shell-Befehle auf dem VPS aus — ausschließlich im Auftrag von Iris. Handelt niemals eigeninitiativ.", Tags = new[] { "Docker", "Shell", "Host", "Deployment" } }, new { Id = "researcher", Name = "Research & Analysis", Model = "deepseek/deepseek-v4-pro", Description = "Spezialisierter Recherche-Agent. Sucht online, prüft Quellen, analysiert Inhalte (inkl. YouTube-Videos) und übergibt strukturierte Erkenntnisse. Ausschließlich Lese- und Analyse-Rechte.", Tags = new[] { "Research", "Quellenprüfung", "Analyse", "Docs" } }, }; var agents = new List(); foreach (var def in agentDefs) { var isActive = false; string? currentTask = null; try { var memDir = "/mnt/workspace-" + def.Id + "/memory"; if (Directory.Exists(memDir)) { var latestFile = Directory.GetFiles(memDir, "*", SearchOption.AllDirectories) .Select(f => new FileInfo(f)) .OrderByDescending(f => f.LastWriteTimeUtc) .FirstOrDefault(); if (latestFile is not null) { var age = DateTime.UtcNow - latestFile.LastWriteTimeUtc; isActive = age.TotalMinutes < 15; if (isActive) { try { var firstLine = File.ReadLines(latestFile.FullName).FirstOrDefault()?.Trim(); if (!string.IsNullOrWhiteSpace(firstLine) && firstLine.Length > 60) currentTask = firstLine[..60]; else if (!string.IsNullOrWhiteSpace(firstLine)) currentTask = firstLine; else currentTask = "Working..."; } catch { currentTask = "Working..."; } } } } } catch { } agents.Add(new DashboardAgentInfo( Id: def.Id, Name: def.Name, Role: DeriveRole(def.Id), Model: def.Model, IsActive: isActive, CurrentTask: currentTask, Description: def.Description, Tags: def.Tags )); } return agents; } public async Task> GetSessionHistoryAsync( string sessionKey, int limit = 50, int offset = 0) { var result = new List(); try { var toolResult = await InvokeToolAsync("sessions_history", new { sessionKey, limit, offset, includeTools = false }); if (toolResult is null) return result; var json = toolResult.ToJsonString(); result.Add(new MessageEntry("diag", "JSON[" + json.Substring(0, Math.Min(200, json.Length)) + "]", DateTimeOffset.UtcNow.ToString("o"))); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; if (!root.TryGetProperty("details", out var detailsEl)) return result; if (!detailsEl.TryGetProperty("messages", out var messagesEl)) return result; if (messagesEl.ValueKind != JsonValueKind.Array) return result; foreach (var msg in messagesEl.EnumerateArray()) { if (!msg.TryGetProperty("role", out var roleEl)) continue; var role = roleEl.GetString() ?? ""; if (role != "user" && role != "assistant") continue; if (!msg.TryGetProperty("content", out var contentEl)) continue; if (contentEl.ValueKind != JsonValueKind.Array) continue; var texts = new List(); foreach (var block in contentEl.EnumerateArray()) { if (!block.TryGetProperty("type", out var typeEl)) continue; if (typeEl.GetString() != "text") continue; if (!block.TryGetProperty("text", out var textEl)) continue; var text = textEl.GetString(); if (!string.IsNullOrWhiteSpace(text)) texts.Add(text); } if (texts.Count == 0) continue; var content = string.Join(" ", texts).Trim(); if (string.IsNullOrWhiteSpace(content)) continue; if (content == "REPLY_SKIP" || content == "ANNOUNCE_SKIP") continue; var ts = DateTimeOffset.UtcNow.ToString("o"); if (msg.TryGetProperty("timestamp", out var tsEl)) ts = tsEl.GetString() ?? ts; result.Add(new MessageEntry(role, content, ts)); } } catch { // return whatever we collected (may be empty) } return result; } public async Task SendChatMessageAsync(string agentId, string message) { try { var result = await InvokeToolAsync("sessions_send", new { agentId, message }); if (result is null) return new ChatResponse(false, null, "Gateway nicht erreichbar"); var details = result["details"]; var ok = (details?["status"]?.GetValue() ?? result["status"]?.GetValue()) == "ok"; var reply = details?["reply"]?.GetValue() ?? result["reply"]?.GetValue() ?? result["response"]?.GetValue() ?? result["content"]?[0]?["text"]?.GetValue(); var error = details?["error"]?.GetValue() ?? result["error"]?.GetValue(); return new ChatResponse(ok, reply, error); } catch (Exception ex) { return new ChatResponse(false, null, "Fehler: " + ex.Message); } } public async Task> GetQueueAsync() { try { var result = await InvokeToolAsync("cron", new { action = "list" }); if (result is null) return new List(); var details = result["details"]; var jobs = details?["jobs"] as JsonArray ?? result.AsArray(); if (jobs is null) return new List(); var items = new List(); foreach (var j in jobs) { if (j is null) continue; var id = j["id"]?.GetValue() ?? ""; var name = j["name"]?.GetValue() ?? id; var status = j["state"]?["lastStatus"]?.GetValue() ?? j["status"]?.GetValue() ?? "unknown"; items.Add(new QueueItem(id, name, status)); } return items; } catch { return new List(); } } public async Task GetStatusAsync() { var gatewayOk = false; var irisStatus = "Offline"; var activeAgents = 0; var pendingTasks = 0; try { using var pingRequest = new HttpRequestMessage(HttpMethod.Get, "/health"); using var pingResponse = await httpClient.SendAsync(pingRequest); gatewayOk = pingResponse.IsSuccessStatusCode; } catch { } if (gatewayOk) { try { var r = await InvokeToolAsync("session_status"); if (r is not null) irisStatus = r["status"]?.GetValue() ?? r["sessionKey"]?.GetValue() ?? "Active"; } catch { } try { var a = await GetAgentsAsync(); activeAgents = a.Count(x => x.IsActive); } catch { } try { var q = await GetQueueAsync(); pendingTasks = q.Count; } catch { } } return new DashboardStatus(gatewayOk, irisStatus, activeAgents, pendingTasks); } public async Task GetAgentModelAsync(string agentId) { try { var result = await InvokeToolAsync("session_status", new { sessionKey = $"agent:{agentId}:main" }); if (result is null) return null; var model = result["model"]?.GetValue(); var provider = result["provider"]?.GetValue(); if (model is null) return null; return new AgentModelInfo(model, provider ?? "unknown"); } catch { return null; } } public async Task SetAgentModelAsync(string agentId, string model) { try { var result = await InvokeToolAsync("session_status", new { sessionKey = $"agent:{agentId}:main", model }); return result is not null; } catch { return false; } } private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch { "iris" => "Chief of Staff", "programmer" => "Full-Stack Developer", "reviewer" => "Code Quality Assurance", "architekt" => "Infrastructure Architect", "executor" => "Host Executor", "researcher" => "Research & Analysis", "main" => "Assistant", _ => "Custom" }; }