Initial commit: Nexus Mission Control Platform
- ASP.NET Core 10 Backend (JWT Auth, Agent config API) - Vue 3 Frontend (Dashboard, Team, Agents, Config Editor) - PostgreSQL Database - Docker Compose setup - Mission Control Dashboard redesign
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Nexus.Api.Domain;
|
||||
using Nexus.Api.Integrations;
|
||||
|
||||
namespace Nexus.Api.Services;
|
||||
|
||||
public sealed record AgentConfig
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("workspace")]
|
||||
public string? Workspace { get; init; }
|
||||
|
||||
[JsonPropertyName("agentDir")]
|
||||
public string? AgentDir { get; init; }
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string? Model { get; init; }
|
||||
|
||||
[JsonPropertyName("identity")]
|
||||
public AgentIdentityConfig? Identity { get; init; }
|
||||
|
||||
[JsonPropertyName("subagents")]
|
||||
public SubAgentConfig? Subagents { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SubAgentConfig
|
||||
{
|
||||
[JsonPropertyName("allowAgents")]
|
||||
public IReadOnlyList<string>? AllowAgents { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AgentIdentityConfig
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("theme")]
|
||||
public string Theme { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record AgentInfo(
|
||||
string Id,
|
||||
string Name,
|
||||
string Role,
|
||||
string Model,
|
||||
OperationalStatus Status,
|
||||
DateTimeOffset? LastSeen,
|
||||
string? Workspace,
|
||||
string? Description
|
||||
);
|
||||
|
||||
public sealed record AgentDetail(
|
||||
string Id,
|
||||
string Name,
|
||||
string Role,
|
||||
string Model,
|
||||
OperationalStatus Status,
|
||||
DateTimeOffset? LastSeen,
|
||||
string? Workspace,
|
||||
string? AgentDir,
|
||||
string? Description,
|
||||
IReadOnlyList<string>? SubAgents,
|
||||
string? IdentityName
|
||||
);
|
||||
|
||||
public interface IAgentService
|
||||
{
|
||||
Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(CancellationToken cancellationToken);
|
||||
Task<AgentDetail?> GetAgentAsync(string id, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class AgentService(IConfiguration configuration, IAgentRuntime runtime) : IAgentService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public async Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var configs = await LoadAgentConfigsAsync(cancellationToken);
|
||||
var runtimeStatus = await runtime.GetStatusAsync(cancellationToken);
|
||||
var overallOperational = runtimeStatus.Status;
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var agents = new List<AgentInfo>(configs.Count);
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var model = config.Model ?? "deepseek/deepseek-v4-flash";
|
||||
var role = DeriveRole(config.Id);
|
||||
var description = config.Identity?.Theme ?? string.Empty;
|
||||
|
||||
// main agent doesn't have a separate identity; set a generic description
|
||||
if (string.IsNullOrEmpty(description))
|
||||
{
|
||||
description = config.Id switch
|
||||
{
|
||||
"main" => "Primary conversational agent — routing and general-purpose chat",
|
||||
_ => description
|
||||
};
|
||||
}
|
||||
|
||||
agents.Add(new AgentInfo(
|
||||
Id: config.Id,
|
||||
Name: config.Identity?.Name ?? config.Name ?? config.Id,
|
||||
Role: role,
|
||||
Model: model,
|
||||
Status: overallOperational,
|
||||
LastSeen: now,
|
||||
Workspace: config.Workspace,
|
||||
Description: description
|
||||
));
|
||||
}
|
||||
|
||||
return agents.AsReadOnly();
|
||||
}
|
||||
|
||||
public async Task<AgentDetail?> GetAgentAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
var configs = await LoadAgentConfigsAsync(cancellationToken);
|
||||
var config = configs.FirstOrDefault(a =>
|
||||
a.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
|
||||
if (config is null) return null;
|
||||
|
||||
var runtimeStatus = await runtime.GetStatusAsync(cancellationToken);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var role = DeriveRole(config.Id);
|
||||
var description = config.Identity?.Theme ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(description) && config.Id == "main")
|
||||
description = "Primary conversational agent — routing and general-purpose chat";
|
||||
|
||||
return new AgentDetail(
|
||||
Id: config.Id,
|
||||
Name: config.Identity?.Name ?? config.Name ?? config.Id,
|
||||
Role: role,
|
||||
Model: config.Model ?? "deepseek/deepseek-v4-flash",
|
||||
Status: runtimeStatus.Status,
|
||||
LastSeen: now,
|
||||
Workspace: config.Workspace,
|
||||
AgentDir: config.AgentDir,
|
||||
Description: description,
|
||||
SubAgents: config.Subagents?.AllowAgents,
|
||||
IdentityName: config.Identity?.Name
|
||||
);
|
||||
}
|
||||
|
||||
private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch
|
||||
{
|
||||
"iris" => "Orchestrator",
|
||||
"programmer" => "Developer",
|
||||
"reviewer" => "Reviewer",
|
||||
"architekt" => "Architect",
|
||||
"main" => "Assistant",
|
||||
_ => "Custom"
|
||||
};
|
||||
|
||||
private async Task<IReadOnlyList<AgentConfig>> LoadAgentConfigsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var path = configuration.GetValue<string>("AgentConfigPath")
|
||||
?? "/home/node/.openclaw/openclaw.json";
|
||||
|
||||
if (!File.Exists(path))
|
||||
return Array.Empty<AgentConfig>();
|
||||
|
||||
var json = await File.ReadAllTextAsync(path, cancellationToken);
|
||||
using var document = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = true });
|
||||
var root = document.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("agents", out var agentsElement))
|
||||
return Array.Empty<AgentConfig>();
|
||||
|
||||
if (!agentsElement.TryGetProperty("list", out var listElement))
|
||||
return Array.Empty<AgentConfig>();
|
||||
|
||||
var defaults = agentsElement.TryGetProperty("defaults", out var defaultsElement)
|
||||
? JsonSerializer.Deserialize<AgentDefaults>(defaultsElement.GetRawText(), JsonOptions)
|
||||
: null;
|
||||
|
||||
var configs = new List<AgentConfig>();
|
||||
foreach (var agentElement in listElement.EnumerateArray())
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<AgentConfig>(agentElement.GetRawText(), JsonOptions);
|
||||
if (config is null || string.IsNullOrWhiteSpace(config.Id))
|
||||
continue;
|
||||
|
||||
// Inherit defaults for missing fields
|
||||
if (string.IsNullOrWhiteSpace(config.Name))
|
||||
config = config with { Name = config.Id };
|
||||
if (string.IsNullOrWhiteSpace(config.Model) && defaults?.Model?.Primary is not null)
|
||||
config = config with { Model = defaults.Model.Primary };
|
||||
if (string.IsNullOrWhiteSpace(config.Workspace) && defaults?.Workspace is not null)
|
||||
config = config with { Workspace = defaults.Workspace };
|
||||
|
||||
configs.Add(config);
|
||||
}
|
||||
|
||||
return configs.AsReadOnly();
|
||||
}
|
||||
|
||||
private sealed record AgentDefaults
|
||||
{
|
||||
[JsonPropertyName("workspace")]
|
||||
public string? Workspace { get; init; }
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public AgentDefaultModel? Model { get; init; }
|
||||
}
|
||||
|
||||
private sealed record AgentDefaultModel
|
||||
{
|
||||
[JsonPropertyName("primary")]
|
||||
public string? Primary { get; init; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user