fix(shadcn): isolate Nexus CSS vars with --nx- prefix + admin password reset endpoint
CI - Build & Test / Backend (.NET) (push) Successful in 26s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 2s

This commit is contained in:
2026-06-11 10:06:53 +02:00
parent a538025049
commit b7b44494f0
59 changed files with 3267 additions and 1153 deletions
+161 -109
View File
@@ -51,7 +51,6 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
return null;
var node = JsonNode.Parse(json);
// Unwrap the { ok: true, result: ... } envelope
if (node?["ok"]?.GetValue<bool>() == true && node["result"] is not null)
return node["result"];
return node;
@@ -62,42 +61,28 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
}
}
/// <summary>
/// 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.
/// </summary>
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<string>();
if (!string.IsNullOrWhiteSpace(text))
{
try { return JsonNode.Parse(text); }
catch { /* fall through */ }
}
}
return result;
}
public async Task<List<DashboardAgentInfo>> GetAgentsAsync()
{
// Hardcoded agent list (known from OpenClaw config).
// Tools/invoke visibility is restricted to agent:main scope,
// so we can't enumerate Iris sessions programmatically.
var agentDefs = new[]
{
new { Id = "iris", Name = "Iris", Model = "GPT-5.4" },
new { Id = "programmer", Name = "Programmer", Model = "DeepSeek V4 Flash" },
new { Id = "reviewer", Name = "Reviewer", Model = "DeepSeek V4 Pro" },
new { Id = "architekt", Name = "DevOps", Model = "DeepSeek V4 Pro" },
new { Id = "executor", Name = "Executor", Model = "DeepSeek V4 Flash" },
new { Id = "researcher", Name = "Researcher", Model = "DeepSeek V4 Pro" },
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>();
@@ -105,28 +90,40 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
{
var isActive = false;
string? currentTask = null;
// Check workspace activity via file timestamps
var memDir = $"/mnt/workspace-{def.Id}/memory";
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)
currentTask = $"Modified: {latestFile.Name}";
{
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 { /* workspace not mounted or inaccessible */ }
catch { }
agents.Add(new DashboardAgentInfo(
Id: def.Id,
@@ -134,68 +131,87 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
Role: DeriveRole(def.Id),
Model: def.Model,
IsActive: isActive,
CurrentTask: currentTask
CurrentTask: currentTask,
Description: def.Description,
Tags: def.Tags
));
}
return agents;
}
public async Task<List<MessageEntry>> GetSessionHistoryAsync(string sessionKey, int limit = 50, int offset = 0)
public async Task<List<MessageEntry>> GetSessionHistoryAsync(
string sessionKey, int limit = 50, int offset = 0)
{
var result = new List<MessageEntry>();
try
{
var result = await InvokeToolAsync("sessions_history", new {
var toolResult = await InvokeToolAsync("sessions_history", new
{
sessionKey, limit, offset,
includeTools = false
});
if (result is null) return new List<MessageEntry>();
if (toolResult is null)
return result;
// sessions_history returns { details: { messages: [...] } }
var messageArray = result["details"]?["messages"] as JsonArray;
if (messageArray is null) return new List<MessageEntry>();
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;
var messages = new List<MessageEntry>();
foreach (var msg in messageArray.Cast<JsonNode?>())
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 is null) continue;
var role = msg["role"]?.GetValue<string>() ?? "";
// Skip non-user/assistant roles
if (role is not ("user" or "assistant")) continue;
if (!msg.TryGetProperty("role", out var roleEl))
continue;
var role = roleEl.GetString() ?? "";
if (role != "user" && role != "assistant")
continue;
// Content is an array of blocks: [{type: "text"/"thinking", text: "..."}]
// Extract only pure text blocks, skip thinking-only messages
var contentBlocks = msg["content"] as JsonArray;
if (contentBlocks is null) continue;
if (!msg.TryGetProperty("content", out var contentEl))
continue;
if (contentEl.ValueKind != JsonValueKind.Array)
continue;
var visibleTexts = new List<string>();
foreach (var block in contentBlocks.Cast<JsonNode?>())
var texts = new List<string>();
foreach (var block in contentEl.EnumerateArray())
{
if (block is null) continue;
var type = block["type"]?.GetValue<string>() ?? "";
var text = block["text"]?.GetValue<string>() ?? "";
if (type == "text" && !string.IsNullOrWhiteSpace(text))
visibleTexts.Add(text);
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);
}
var visibleContent = string.Join(" ", visibleTexts).Trim();
if (string.IsNullOrWhiteSpace(visibleContent)) continue;
if (texts.Count == 0)
continue;
// Skip system-only replies
if (visibleContent is "REPLY_SKIP" or "ANNOUNCE_SKIP") continue;
var content = string.Join(" ", texts).Trim();
if (string.IsNullOrWhiteSpace(content))
continue;
if (content == "REPLY_SKIP" || content == "ANNOUNCE_SKIP")
continue;
var timestamp = msg["timestamp"]?.GetValue<string>()
?? DateTimeOffset.UtcNow.ToString("o");
var ts = DateTimeOffset.UtcNow.ToString("o");
if (msg.TryGetProperty("timestamp", out var tsEl))
ts = tsEl.GetString() ?? ts;
messages.Add(new MessageEntry(role, visibleContent, timestamp));
result.Add(new MessageEntry(role, content, ts));
}
return messages;
}
catch
{
return new List<MessageEntry>();
// return whatever we collected (may be empty)
}
return result;
}
public async Task<ChatResponse> SendChatMessageAsync(string agentId, string message)
@@ -203,22 +219,24 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
try
{
var result = await InvokeToolAsync("sessions_send", new { agentId, message });
if (result is null) return new ChatResponse(false, null, "Gateway nicht erreichbar");
if (result is null)
return new ChatResponse(false, null, "Gateway nicht erreichbar");
// sessions_send reply is in details.reply or content[0].text
var details = result["details"];
var ok = (details?["status"]?.GetValue<string>() ?? result["status"]?.GetValue<string>()) == "ok";
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>();
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}");
return new ChatResponse(false, null, "Fehler: " + ex.Message);
}
}
@@ -227,13 +245,15 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
try
{
var result = await InvokeToolAsync("cron", new { action = "list" });
var data = ExtractToolData(result);
if (data is null) return new List<QueueItem>();
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>();
var jobs = data["jobs"] as JsonArray ?? data.AsArray();
if (jobs is null) return items;
foreach (var j in jobs)
{
if (j is null) continue;
@@ -242,7 +262,6 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
var status = j["state"]?["lastStatus"]?.GetValue<string>()
?? j["status"]?.GetValue<string>()
?? "unknown";
items.Add(new QueueItem(id, name, status));
}
return items;
@@ -260,46 +279,37 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
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
}
catch { }
if (gatewayOk)
{
// Step 2: Session status
try
{
var sessionResult = await InvokeToolAsync("session_status");
if (sessionResult is not null)
{
irisStatus = sessionResult["status"]?.GetValue<string>()
?? sessionResult["sessionKey"]?.GetValue<string>()
var r = await InvokeToolAsync("session_status");
if (r is not null)
irisStatus = r["status"]?.GetValue<string>()
?? r["sessionKey"]?.GetValue<string>()
?? "Active";
}
}
catch { }
// Step 3: Active agents
try
{
var agents = await GetAgentsAsync();
activeAgents = agents.Count(a => a.IsActive);
var a = await GetAgentsAsync();
activeAgents = a.Count(x => x.IsActive);
}
catch { }
// Step 4: Queue items
try
{
var queue = await GetQueueAsync();
pendingTasks = queue.Count;
var q = await GetQueueAsync();
pendingTasks = q.Count;
}
catch { }
}
@@ -307,14 +317,56 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
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" => "Orchestrator",
"programmer" => "Developer",
"reviewer" => "Reviewer",
"architekt" => "Architect",
"executor" => "Executor",
"researcher" => "Researcher",
"iris" => "Chief of Staff",
"programmer" => "Full-Stack Developer",
"reviewer" => "Code Quality Assurance",
"architekt" => "Infrastructure Architect",
"executor" => "Host Executor",
"researcher" => "Research & Analysis",
"main" => "Assistant",
_ => "Custom"
};