a79d8282dc
- 15 Controller-Klassen ersetzen Minimal APIs in Program.cs - Repository Pattern mit Interfaces + Implementierungen (Project, Task, Activity, User) - AuthService verwendet jetzt IUserRepository statt direktem DbContext-Zugriff - SecurityHeadersMiddleware als eigenständige Middleware-Klasse - PathSecurityHelper als gemeinsamer Helper für Pfadvalidierung - DTOs in eigenem Namespace Nexus.Api.DTOs - EF-Entities in Nexus.Api.Data (vorher Nexus.Api.Domain) - Program.cs auf DI-Registrierung + Middleware reduziert - Alle 43 Endpoints unverändert erhalten - Build + 3/3 Tests erfolgreich
222 lines
7.2 KiB
C#
222 lines
7.2 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Nexus.Api.Data;
|
|
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;
|
|
|
|
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; }
|
|
}
|
|
}
|