- ClawdDotNet.Web: Kestrel im Prozess der Anwendung, Token-Absicherung (TokenGate mit FixedTimeEquals), Endpunkte unter /api, Ereignisstrom unter /events (Server-Sent Events) und fortlaufendes Log-Streaming. - Weboberflaeche (wwwroot/): Vollstaendiges Single-Page-Frontend mit nativem HTML5, CSS3 und ES-Modulen ohne Buildschritt (strikt textContent, #-Routing, sofortiger Login-Sprung bei fehlendem Token, Light/Dark-Theme, lokale Barlow-Schriften). - ClawdDotNet.App: Fachlogik aus den Avalonia-ViewModels in Dienste herausgeloest (AgentAdministration, JobAdministration, ServiceAdministration, RunCoordinator) und ueber InstanceChange synchronisiert. - Tests: ClawdDotNet.Web.Tests und ClawdDotNet.App.Tests hinzugefuegt, alle 684 Tests erfolgreich.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
namespace ClawdDotNet.Web.Contracts;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Die Übertragungstypen der Weboberfläche.
|
||||
//
|
||||
// Warum es sie gibt, statt die Kerntypen zu serialisieren:
|
||||
//
|
||||
// 1. AgentConfig trägt Zugangsdaten. Ein `return agent;` hätte den
|
||||
// Werkzeug-Konfigurationsbaum samt Passwörtern ins Netz geschickt — die Sorte
|
||||
// Fehler, die niemandem auffällt, weil alles funktioniert.
|
||||
// 2. Die Kerntypen ändern sich mit dem Kern. Ein neues Feld in TaskItem wäre sonst
|
||||
// still eine API-Änderung, auf die sich jemand verlassen hat.
|
||||
// 3. Die Grenze soll einen Prozesswechsel überstehen (siehe WebUiHost). Über eine
|
||||
// Prozessgrenze geht nur, was hier steht.
|
||||
//
|
||||
// Namen auf Englisch in camelCase wie im übrigen Code; die Beschreibungen auf Deutsch.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Antwort auf einen verwaltenden Aufruf — dieselbe Form wie <c>AdminResult</c>.</summary>
|
||||
public sealed record OutcomeDto(bool Ok, string Message);
|
||||
|
||||
/// <summary>Version, Bau und Instanz. Was auf der Info-Seite steht.</summary>
|
||||
public sealed record InfoDto(
|
||||
string AppVersion,
|
||||
string BuildSummary,
|
||||
string InstanceId,
|
||||
string InstanceName,
|
||||
bool EngineActive,
|
||||
bool SchedulerActive,
|
||||
bool StagingActive,
|
||||
bool DeploymentcenterActive);
|
||||
|
||||
/// <summary>Erreichbarkeit und Guthaben bei OpenRouter.</summary>
|
||||
public sealed record StatusDto(
|
||||
bool ApiReachable,
|
||||
string StatusText,
|
||||
string CreditsText,
|
||||
double? CreditBalance,
|
||||
double? CreditRemaining);
|
||||
|
||||
/// <summary>Ein Agent, ohne alles, was ein Zugangsdatum sein könnte.</summary>
|
||||
public sealed record AgentDto(
|
||||
string AgentId,
|
||||
string DisplayName,
|
||||
string Model,
|
||||
string Description,
|
||||
string PromptCaching,
|
||||
int MaxToolResultChars,
|
||||
int MaxSteps,
|
||||
int MaxContextTokens,
|
||||
int TimeoutSeconds,
|
||||
decimal DailyCostUsd,
|
||||
long DailyTokens,
|
||||
bool IsRunning,
|
||||
IReadOnlyList<AgentToolDto> Tools);
|
||||
|
||||
/// <summary>
|
||||
/// Ein Werkzeug am Agenten. <b>Ohne die Einstellungen</b> — die stehen in einem freien
|
||||
/// Wörterbuch und enthalten regelmäßig Passwörter und Schlüssel. Was einstellbar ist und
|
||||
/// ob etwas gesetzt ist, sagt <see cref="ToolSettingsDto"/>.
|
||||
/// </summary>
|
||||
public sealed record AgentToolDto(string Name, string Description, bool Enabled);
|
||||
|
||||
/// <summary>
|
||||
/// Die Einstellungen eines Werkzeugs, maskiert.
|
||||
///
|
||||
/// <para>Ein Wert, dessen Name ein Zugangsdatum bezeichnet, kommt nie heraus. Statt
|
||||
/// seiner steht in <see cref="Secrets"/>, ob überhaupt etwas gesetzt ist — mehr braucht
|
||||
/// eine Anzeige nicht, und mehr darf sie nicht wissen.</para>
|
||||
/// </summary>
|
||||
public sealed record ToolSettingsDto(
|
||||
string Name,
|
||||
IReadOnlyDictionary<string, object?> Values,
|
||||
IReadOnlyDictionary<string, bool> Secrets);
|
||||
|
||||
/// <summary>Eine Zeile im Chatverlauf.</summary>
|
||||
public sealed record ChatEntryDto(
|
||||
string Role,
|
||||
string Content,
|
||||
DateTime Timestamp,
|
||||
string? Source);
|
||||
|
||||
/// <summary>Was der Browser schickt, wenn jemand etwas in den Chat tippt.</summary>
|
||||
public sealed record SendMessageDto(string Message);
|
||||
|
||||
/// <summary>Ein geplanter Lauf.</summary>
|
||||
public sealed record JobDto(
|
||||
string Kind,
|
||||
string JobId,
|
||||
string AgentId,
|
||||
string AgentName,
|
||||
string ToolName,
|
||||
string Cron,
|
||||
string Message,
|
||||
bool RunOnStart,
|
||||
DateTime? NextRunUtc,
|
||||
DateTime? LastRunLocal,
|
||||
bool CronValid,
|
||||
bool Enabled);
|
||||
|
||||
/// <summary>Was einen geplanten Lauf ausmacht — beim Anlegen wie beim Ändern.</summary>
|
||||
public sealed record JobInputDto(
|
||||
string AgentId,
|
||||
string Kind,
|
||||
string ToolName = "",
|
||||
string JobTypeId = "",
|
||||
string Cron = "",
|
||||
string TaskMessage = "",
|
||||
bool RunOnStart = false);
|
||||
|
||||
/// <summary>Ein Dienst der Instanz.</summary>
|
||||
public sealed record ServiceDto(
|
||||
string ServiceId,
|
||||
string Name,
|
||||
string Type,
|
||||
int Port,
|
||||
bool Enabled,
|
||||
bool BuiltIn,
|
||||
string Description);
|
||||
|
||||
/// <summary>Was einen neuen Dienst ausmacht.</summary>
|
||||
public sealed record ServiceInputDto(string Name, string Type, int Port, string Description = "");
|
||||
|
||||
/// <summary>Ein neuer Agent.</summary>
|
||||
public sealed record NewAgentDto(string AgentId, string DisplayName, string Model, string Description = "");
|
||||
|
||||
/// <summary>Eine Aufgabe vom Taskboard.</summary>
|
||||
public sealed record TaskDto(
|
||||
string Id,
|
||||
string Title,
|
||||
string Status,
|
||||
string Type,
|
||||
int Priority,
|
||||
string Assignee,
|
||||
bool RequireApproval,
|
||||
string Acceptance,
|
||||
IReadOnlyList<string> BlockedBy,
|
||||
string Body,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt);
|
||||
|
||||
/// <summary>Ein eingefrorener Aufruf, der auf Freigabe wartet.</summary>
|
||||
public sealed record StagedCallDto(
|
||||
long Id,
|
||||
string RunId,
|
||||
string AgentId,
|
||||
string Tool,
|
||||
string? Action,
|
||||
string Proposal,
|
||||
string Status,
|
||||
string ArgumentsJson);
|
||||
|
||||
/// <summary>Warum ein Vorschlag abgelehnt wurde.</summary>
|
||||
public sealed record RejectDto(string Reason);
|
||||
|
||||
/// <summary>
|
||||
/// Ein Lauf mit dem, was er gekostet hat.
|
||||
///
|
||||
/// <para><see cref="CostIsKnown"/> ist nicht schmückendes Beiwerk: Ohne geladene
|
||||
/// Preisliste steht in <see cref="CostUsd"/> eine Null, die „kostenlos" heißen könnte
|
||||
/// und „unbekannt" heißt. Eine Anzeige, die das nicht unterscheidet, lügt.</para>
|
||||
/// </summary>
|
||||
public sealed record UsageDto(
|
||||
long Id,
|
||||
string AgentId,
|
||||
string Model,
|
||||
int PromptTokens,
|
||||
int CompletionTokens,
|
||||
int CachedTokens,
|
||||
decimal CostUsd,
|
||||
bool CostIsKnown,
|
||||
string Status,
|
||||
int StepCount,
|
||||
DateTime OccurredAt);
|
||||
|
||||
/// <summary>Eine Protokollzeile.</summary>
|
||||
public sealed record LogLineDto(string Module, string Level, string Text);
|
||||
@@ -0,0 +1,156 @@
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Core.Accounting;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using ClawdDotNet.Core.Security;
|
||||
using ClawdDotNet.Core.Staging;
|
||||
using ClawdDotNet.Core.Tasks;
|
||||
|
||||
namespace ClawdDotNet.Web.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Die eine Stelle, an der Kerntypen zu Übertragungstypen werden.
|
||||
///
|
||||
/// <para>Eine Stelle, weil die Maskierung von Zugangsdaten hier hängt. Verstreut über
|
||||
/// zehn Endpunkte wäre die Frage „kommt hier ein Passwort heraus?" nicht mehr zu
|
||||
/// beantworten, sondern nur noch zu hoffen.</para>
|
||||
/// </summary>
|
||||
internal static class Mapping
|
||||
{
|
||||
public static AgentDto ToDto(
|
||||
this AgentConfig agent,
|
||||
IReadOnlyDictionary<string, string> toolDescriptions,
|
||||
bool isRunning)
|
||||
=> new(
|
||||
agent.AgentId,
|
||||
agent.DisplayName,
|
||||
agent.Model,
|
||||
agent.Description,
|
||||
agent.PromptCaching,
|
||||
agent.MaxToolResultChars,
|
||||
agent.LoopGuard.MaxSteps,
|
||||
agent.LoopGuard.MaxContextTokens,
|
||||
agent.LoopGuard.TimeoutSeconds,
|
||||
agent.Budget.DailyCostUsd,
|
||||
agent.Budget.DailyTokens,
|
||||
isRunning,
|
||||
AgentAdministration.KnownTools
|
||||
.Select(name => new AgentToolDto(
|
||||
name,
|
||||
toolDescriptions.GetValueOrDefault(name) ?? "",
|
||||
agent.Tools.ContainsKey(name)))
|
||||
.ToList());
|
||||
|
||||
/// <summary>
|
||||
/// Die Einstellungen eines Werkzeugs — <b>ohne die Geheimnisse</b>.
|
||||
///
|
||||
/// <para>Welcher Schlüssel eines ist, entscheidet <see cref="ConfigSecrets"/>: der
|
||||
/// Kern weiß es bereits, und eine zweite Liste hier wäre eine, die beim nächsten
|
||||
/// neuen Werkzeug vergessen wird. Statt des Werts steht in <c>Secrets</c>, ob
|
||||
/// überhaupt einer gesetzt ist.</para>
|
||||
/// </summary>
|
||||
public static ToolSettingsDto ToDto(
|
||||
string toolName, IReadOnlyDictionary<string, object?> settings)
|
||||
{
|
||||
var values = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
var secrets = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var (key, value) in settings)
|
||||
{
|
||||
if (ConfigSecrets.IsSecretKey(key))
|
||||
secrets[key] = value is not null && value is not string { Length: 0 };
|
||||
else
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
return new ToolSettingsDto(toolName, values, secrets);
|
||||
}
|
||||
|
||||
public static ChatEntryDto ToDto(this ChatEntry entry)
|
||||
=> new(entry.Role, entry.Content, entry.Timestamp, entry.Source);
|
||||
|
||||
public static JobDto ToDto(this JobSummary job, DateTime? lastRun)
|
||||
=> new(
|
||||
job.Kind == JobKind.ToolJob ? "toolJob" : "agentWakeup",
|
||||
job.JobId,
|
||||
job.AgentId,
|
||||
job.AgentName,
|
||||
job.ToolName,
|
||||
job.Cron,
|
||||
job.Message,
|
||||
job.RunOnStart,
|
||||
job.NextRunUtc,
|
||||
lastRun,
|
||||
job.CronValid,
|
||||
job.Enabled);
|
||||
|
||||
public static JobDefinition ToDefinition(this JobInputDto input)
|
||||
=> new(
|
||||
input.AgentId,
|
||||
string.Equals(input.Kind, "toolJob", StringComparison.OrdinalIgnoreCase)
|
||||
? JobKind.ToolJob
|
||||
: JobKind.AgentWakeup,
|
||||
input.ToolName,
|
||||
input.JobTypeId,
|
||||
input.Cron,
|
||||
input.TaskMessage,
|
||||
input.RunOnStart);
|
||||
|
||||
public static ServiceDto ToDto(this ServiceConfig service)
|
||||
=> new(
|
||||
service.ServiceId,
|
||||
service.Name,
|
||||
service.Type,
|
||||
service.Port,
|
||||
service.Enabled,
|
||||
service.BuiltIn,
|
||||
service.Description);
|
||||
|
||||
public static TaskDto ToDto(this TaskItem task)
|
||||
=> new(
|
||||
task.Id,
|
||||
task.Title,
|
||||
task.Status.ToString(),
|
||||
task.Type.ToString(),
|
||||
task.Priority,
|
||||
task.Assignee,
|
||||
task.RequireApproval,
|
||||
task.Acceptance,
|
||||
task.BlockedBy,
|
||||
task.Body,
|
||||
task.CreatedAt,
|
||||
task.UpdatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Ein wartender Aufruf samt seiner eingefrorenen Argumente.
|
||||
///
|
||||
/// <para>Die Argumente gehen <b>roh</b> heraus, und das ist der ganze Sinn der
|
||||
/// Freigabe: Wer entscheidet, muss sehen, was genau ausgeführt würde — nicht eine
|
||||
/// Zusammenfassung davon. Sie sind zugleich Fremdtext aus einem Modell; die Anzeige
|
||||
/// setzt sie als Text ein, nie als HTML.</para>
|
||||
/// </summary>
|
||||
public static StagedCallDto ToDto(this StagedCall call)
|
||||
=> new(
|
||||
call.Id,
|
||||
call.RunId,
|
||||
call.AgentId,
|
||||
call.Tool,
|
||||
call.Action,
|
||||
call.Proposal,
|
||||
call.Status.ToString(),
|
||||
call.ArgumentsJson);
|
||||
|
||||
public static UsageDto ToDto(this RunUsage usage)
|
||||
=> new(
|
||||
usage.Id,
|
||||
usage.AgentId,
|
||||
usage.Model,
|
||||
usage.PromptTokens,
|
||||
usage.CompletionTokens,
|
||||
usage.CachedTokens,
|
||||
usage.CostUsd,
|
||||
usage.CostIsKnown,
|
||||
usage.Status,
|
||||
usage.StepCount,
|
||||
usage.OccurredAt);
|
||||
}
|
||||
Reference in New Issue
Block a user