374 lines
14 KiB
C#
374 lines
14 KiB
C#
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<JsonNode?> InvokeToolAsync(string tool, object? args = null)
|
|
{
|
|
try
|
|
{
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, "/tools/invoke");
|
|
ApplyAuth(request);
|
|
|
|
var body = new Dictionary<string, object?> { ["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<bool>() == true && node["result"] is not null)
|
|
return node["result"];
|
|
return node;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<List<DashboardAgentInfo>> 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<DashboardAgentInfo>();
|
|
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<List<MessageEntry>> GetSessionHistoryAsync(
|
|
string sessionKey, int limit = 50, int offset = 0)
|
|
{
|
|
var result = new List<MessageEntry>();
|
|
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<string>();
|
|
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<ChatResponse> 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<string>()
|
|
?? result["status"]?.GetValue<string>()) == "ok";
|
|
var reply = details?["reply"]?.GetValue<string>()
|
|
?? result["reply"]?.GetValue<string>()
|
|
?? result["response"]?.GetValue<string>()
|
|
?? result["content"]?[0]?["text"]?.GetValue<string>();
|
|
var error = details?["error"]?.GetValue<string>()
|
|
?? result["error"]?.GetValue<string>();
|
|
|
|
return new ChatResponse(ok, reply, error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ChatResponse(false, null, "Fehler: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
public async Task<List<QueueItem>> GetQueueAsync()
|
|
{
|
|
try
|
|
{
|
|
var result = await InvokeToolAsync("cron", new { action = "list" });
|
|
if (result is null)
|
|
return new List<QueueItem>();
|
|
|
|
var details = result["details"];
|
|
var jobs = details?["jobs"] as JsonArray ?? result.AsArray();
|
|
if (jobs is null)
|
|
return new List<QueueItem>();
|
|
|
|
var items = new List<QueueItem>();
|
|
foreach (var j in jobs)
|
|
{
|
|
if (j is null) continue;
|
|
var id = j["id"]?.GetValue<string>() ?? "";
|
|
var name = j["name"]?.GetValue<string>() ?? id;
|
|
var status = j["state"]?["lastStatus"]?.GetValue<string>()
|
|
?? j["status"]?.GetValue<string>()
|
|
?? "unknown";
|
|
items.Add(new QueueItem(id, name, status));
|
|
}
|
|
return items;
|
|
}
|
|
catch
|
|
{
|
|
return new List<QueueItem>();
|
|
}
|
|
}
|
|
|
|
public async Task<DashboardStatus> 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<string>()
|
|
?? r["sessionKey"]?.GetValue<string>()
|
|
?? "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<AgentModelInfo?> 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<string>();
|
|
var provider = result["provider"]?.GetValue<string>();
|
|
|
|
if (model is null)
|
|
return null;
|
|
|
|
return new AgentModelInfo(model, provider ?? "unknown");
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> 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"
|
|
};
|
|
}
|