fix(shadcn): isolate Nexus CSS vars with --nx- prefix + admin password reset endpoint
This commit is contained in:
@@ -91,6 +91,23 @@ public class AuthController(
|
||||
: Results.Ok(new UserInfo { Id = user.Id, Email = user.Email, DisplayName = user.DisplayName, Role = user.Role });
|
||||
}
|
||||
|
||||
[HttpPost("admin-reset-password")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<IResult> AdminResetPassword([FromBody] AdminResetPasswordRequest request, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.NewPassword) || string.IsNullOrWhiteSpace(request.AdminToken))
|
||||
return Results.ValidationProblem(new Dictionary<string, string[]> { ["request"] = ["Email, new password, and admin token are required."] });
|
||||
|
||||
if (request.NewPassword.Length < 10)
|
||||
return Results.ValidationProblem(new Dictionary<string, string[]> { ["newPassword"] = ["New password must be at least 10 characters."] });
|
||||
|
||||
var success = await authService.AdminResetPasswordAsync(request.Email, request.NewPassword, request.AdminToken, ct);
|
||||
if (!success)
|
||||
return Results.Problem("Password reset failed. Check the admin token, email, and that the user exists.", statusCode: 400);
|
||||
|
||||
return Results.Ok(new { message = "Password reset successfully." });
|
||||
}
|
||||
|
||||
[HttpPost("change-password")]
|
||||
public async Task<IResult> ChangePassword([FromBody] ChangePasswordRequest request, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -122,7 +122,6 @@ public class DashboardController(OpenClawGatewayClient gateway, ILogger<Dashboar
|
||||
var key = string.IsNullOrWhiteSpace(sessionKey) ? "agent:iris:main" : sessionKey.Trim();
|
||||
var messages = await gateway.GetSessionHistoryAsync(key, Math.Clamp(limit, 1, 200), Math.Max(0, offset));
|
||||
|
||||
// Filter: only user and assistant messages (exclude tool/system)
|
||||
return messages
|
||||
.Where(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -152,6 +151,66 @@ public class DashboardController(OpenClawGatewayClient gateway, ILogger<Dashboar
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current model and provider for a specific agent session.
|
||||
/// Calls session_status with the agent's session key.
|
||||
/// </summary>
|
||||
[HttpGet("agents/{id}/model")]
|
||||
public async Task<ActionResult<AgentModelInfo>> GetAgentModel(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await gateway.GetAgentModelAsync(id);
|
||||
if (info is null)
|
||||
return NotFound(new { error = $"Agent '{id}' not found or gateway unreachable" });
|
||||
return Ok(info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "GetAgentModel failed for {AgentId}", id);
|
||||
return StatusCode(500, new { error = "Internal error" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the model for a specific agent session.
|
||||
/// Calls session_status with model parameter.
|
||||
/// </summary>
|
||||
[HttpPut("agents/{id}/model")]
|
||||
public async Task<ActionResult> SetAgentModel(string id, [FromBody] SetModelRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Model))
|
||||
return BadRequest(new { error = "Model is required" });
|
||||
|
||||
try
|
||||
{
|
||||
var ok = await gateway.SetAgentModelAsync(id, request.Model);
|
||||
if (!ok)
|
||||
return StatusCode(502, new { error = "Gateway did not accept the change" });
|
||||
return Ok(new { status = "ok", model = request.Model });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "SetAgentModel failed for {AgentId}", id);
|
||||
return StatusCode(500, new { error = "Internal error" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of available models that can be assigned to agents.
|
||||
/// </summary>
|
||||
[HttpGet("models")]
|
||||
public ActionResult<List<ModelOption>> GetAvailableModels()
|
||||
{
|
||||
var models = new List<ModelOption>
|
||||
{
|
||||
new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"),
|
||||
new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"),
|
||||
new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek")
|
||||
};
|
||||
return Ok(models);
|
||||
}
|
||||
|
||||
// ========== Helpers ==========
|
||||
|
||||
private static DateTimeOffset ParseTimestamp(string timestamp)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Nexus.Api.DTOs;
|
||||
|
||||
public sealed record AdminResetPasswordRequest
|
||||
{
|
||||
/// <summary>The email of the user whose password should be reset.</summary>
|
||||
public required string Email { get; init; }
|
||||
|
||||
/// <summary>The new password to set.</summary>
|
||||
public required string NewPassword { get; init; }
|
||||
|
||||
/// <summary>Admin reset token from configuration (Admin:ResetToken).</summary>
|
||||
public required string AdminToken { get; init; }
|
||||
}
|
||||
@@ -6,7 +6,9 @@ public sealed record DashboardAgentInfo(
|
||||
string Role,
|
||||
string Model,
|
||||
bool IsActive,
|
||||
string? CurrentTask
|
||||
string? CurrentTask,
|
||||
string? Description,
|
||||
string[] Tags
|
||||
);
|
||||
|
||||
public sealed record MessageEntry(
|
||||
@@ -45,3 +47,18 @@ public sealed record QueueItem(
|
||||
string Name,
|
||||
string Status
|
||||
);
|
||||
|
||||
public sealed record AgentModelInfo(
|
||||
string Model,
|
||||
string Provider
|
||||
);
|
||||
|
||||
public sealed record SetModelRequest(
|
||||
string Model
|
||||
);
|
||||
|
||||
public sealed record ModelOption(
|
||||
string Id,
|
||||
string Name,
|
||||
string Provider
|
||||
);
|
||||
|
||||
+3
-3
@@ -102,21 +102,21 @@ builder.Services.AddHttpClient<IAgentRuntime, OpenClawRuntime>(client =>
|
||||
{
|
||||
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
||||
?? "http://127.0.0.1:18789");
|
||||
client.Timeout = TimeSpan.FromSeconds(5);
|
||||
client.Timeout = TimeSpan.FromSeconds(120);
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient("gateway", client =>
|
||||
{
|
||||
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
||||
?? "http://127.0.0.1:18789");
|
||||
client.Timeout = TimeSpan.FromSeconds(5);
|
||||
client.Timeout = TimeSpan.FromSeconds(120);
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient<OpenClawGatewayClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
||||
?? "http://127.0.0.1:18789");
|
||||
client.Timeout = TimeSpan.FromSeconds(5);
|
||||
client.Timeout = TimeSpan.FromSeconds(120);
|
||||
});
|
||||
|
||||
// --- Application Services ---
|
||||
|
||||
@@ -17,6 +17,7 @@ public interface IAuthService
|
||||
Task<NexusUser?> GetUserAsync(Guid userId, CancellationToken ct = default);
|
||||
Task<NexusUser?> UpdateProfileAsync(Guid userId, UpdateProfileRequest request, CancellationToken ct = default);
|
||||
Task<bool> ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken ct = default);
|
||||
Task<bool> AdminResetPasswordAsync(string email, string newPassword, string adminToken, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record AuthSession(
|
||||
@@ -31,6 +32,8 @@ public sealed class AuthService : IAuthService
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
|
||||
private static string AdminResetToken => Environment.GetEnvironmentVariable("Admin__ResetToken") ?? string.Empty;
|
||||
|
||||
public AuthService(IUserRepository users, IConfiguration config, ILogger<AuthService> logger)
|
||||
{
|
||||
_users = users;
|
||||
@@ -128,6 +131,46 @@ public sealed class AuthService : IAuthService
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> AdminResetPasswordAsync(string email, string newPassword, string adminToken, CancellationToken ct = default)
|
||||
{
|
||||
// Validate admin token
|
||||
if (string.IsNullOrWhiteSpace(adminToken) || string.IsNullOrWhiteSpace(AdminResetToken))
|
||||
{
|
||||
_logger.LogWarning("Admin password reset attempted without admin token or token not configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(adminToken),
|
||||
Encoding.UTF8.GetBytes(AdminResetToken)))
|
||||
{
|
||||
_logger.LogWarning("Invalid admin reset token provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(newPassword))
|
||||
return false;
|
||||
|
||||
if (newPassword.Length < 10)
|
||||
return false;
|
||||
|
||||
var normalizedEmail = NormalizeEmail(email);
|
||||
var user = await _users.GetByEmailAsync(normalizedEmail, ct);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
_logger.LogWarning("Admin password reset: user {Email} not found", email);
|
||||
return false;
|
||||
}
|
||||
|
||||
user.PasswordHash = PasswordSecurity.Hash(newPassword);
|
||||
user.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
await _users.UpdateAsync(user, ct);
|
||||
|
||||
_logger.LogInformation("Admin password reset completed for {Email}", email);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<AuthSession?> CreateSessionAsync(
|
||||
NexusUser user,
|
||||
Guid familyId,
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user