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); // Unwrap the { ok: true, result: ... } envelope if (node?["ok"]?.GetValue() == true && node["result"] is not null) return node["result"]; return node; } catch { return null; } } /// /// Extracts useful data from a tool response that may embed JSON in content[0].text. /// Returns the unwrapped "details" object if present, otherwise the raw result. /// private static JsonNode? ExtractToolData(JsonNode? result) { if (result is null) return null; // Some tools return { details: {...}, content: [...] } if (result["details"] is JsonNode details && details is not JsonValue) return details; // Some tools wrap in content[0].text as JSON string if (result["content"] is JsonArray content && content.Count > 0) { var text = content[0]?["text"]?.GetValue(); if (!string.IsNullOrWhiteSpace(text)) { try { return JsonNode.Parse(text); } catch { /* fall through */ } } } return result; } public async Task> GetAgentsAsync() { var agents = new List(); var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); // Get sessions list var sessionsResult = await InvokeToolAsync("sessions_list"); var sessionsData = ExtractToolData(sessionsResult); if (sessionsData is JsonObject so) { // sessions_list returns { agents: [...], sessions: {...} } // Try agents array first (from gateway call) if (so["agents"] is JsonArray agentArray) { foreach (var a in agentArray) { if (a is null) continue; var id = a["agentId"]?.GetValue() ?? ""; if (string.IsNullOrWhiteSpace(id) || !seenIds.Add(id)) continue; var name = a["name"]?.GetValue() ?? id; var model = "GPT-5.4"; // default, overridden if available var isActive = false; var currentTask = (string?)null; // Check if agent has recent sessions (active indicator) if (a["sessions"] is JsonObject sess && sess["recent"] is JsonArray recent && recent.Count > 0) { var age = recent[0]?["age"]?.GetValue() ?? long.MaxValue; isActive = age < 300_000; // active if session < 5 min ago currentTask = recent[0]?["key"]?.GetValue(); } agents.Add(new DashboardAgentInfo(id, name, DeriveRole(id), model, isActive, currentTask)); } } } // Also get subagents list var subResult = await InvokeToolAsync("subagents", new { action = "list" }); var subData = ExtractToolData(subResult); if (subData is JsonArray subArray) { foreach (var s in subArray) { if (s is null) continue; var id = s["id"]?.GetValue() ?? s["agentId"]?.GetValue() ?? ""; if (string.IsNullOrWhiteSpace(id) || !seenIds.Add(id)) continue; var name = s["name"]?.GetValue() ?? id; var status = s["status"]?.GetValue() ?? ""; var isActive = status is "active" or "running"; var currentTask = s["task"]?.GetValue() ?? s["currentTask"]?.GetValue(); agents.Add(new DashboardAgentInfo(id, name, DeriveRole(id), "", isActive, currentTask)); } } return agents; } public async Task> GetSessionHistoryAsync(string sessionKey, int limit = 50, int offset = 0) { try { var result = await InvokeToolAsync("sessions_history", new { sessionKey, limit, offset }); if (result is null) return new List(); var messages = new List(); var array = result as JsonArray ?? result.AsArray(); if (array is null) return messages; foreach (var msg in array) { if (msg is null) continue; var role = msg["role"]?.GetValue() ?? ""; var content = msg["content"]?.GetValue() ?? ""; var timestamp = msg["timestamp"]?.GetValue() ?? msg["ts"]?.GetValue() ?? msg["createdAt"]?.GetValue() ?? DateTimeOffset.UtcNow.ToString("o"); messages.Add(new MessageEntry(role, content, timestamp)); } return messages; } catch { return new List(); } } public async Task SendChatMessageAsync(string sessionKey, string message) { try { var result = await InvokeToolAsync("sessions_send", new { sessionKey, message }); if (result is null) return new ChatResponse(false, null, "Gateway nicht erreichbar"); var ok = result["ok"]?.GetValue() ?? false; var reply = result["reply"]?.GetValue() ?? result["response"]?.GetValue() ?? result["content"]?[0]?["text"]?.GetValue(); var error = 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" }); var data = ExtractToolData(result); if (data is null) return new List(); var items = new List(); var jobs = data["jobs"] as JsonArray ?? data.AsArray(); if (jobs is null) return items; 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; // Step 1: Health check (no auth needed) try { using var pingRequest = new HttpRequestMessage(HttpMethod.Get, "/health"); using var pingResponse = await httpClient.SendAsync(pingRequest); gatewayOk = pingResponse.IsSuccessStatusCode; } catch { // gatewayOk stays false } if (gatewayOk) { // Step 2: Session status try { var sessionResult = await InvokeToolAsync("session_status"); if (sessionResult is not null) { irisStatus = sessionResult["status"]?.GetValue() ?? sessionResult["sessionKey"]?.GetValue() ?? "Active"; } } catch { } // Step 3: Active agents try { var agents = await GetAgentsAsync(); activeAgents = agents.Count(a => a.IsActive); } catch { } // Step 4: Queue items try { var queue = await GetQueueAsync(); pendingTasks = queue.Count; } catch { } } return new DashboardStatus(gatewayOk, irisStatus, activeAgents, pendingTasks); } private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch { "iris" => "Orchestrator", "programmer" => "Developer", "reviewer" => "Reviewer", "architekt" => "Architect", "executor" => "Executor", "researcher" => "Researcher", "main" => "Assistant", _ => "Custom" }; }