- 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:
@@ -1,5 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
@@ -77,12 +79,11 @@ public sealed partial class AgentItemViewModel : ObservableObject
|
||||
|
||||
public ObservableCollection<ToolItemViewModel> AvailableTools { get; } = [];
|
||||
|
||||
public static readonly List<string> KnownTools =
|
||||
[
|
||||
"FileRW", "Telegram", "Mail", "Database", "FTP",
|
||||
"DirectAPI", "WebFetch", "WebMonitor", "AgentComm",
|
||||
"SocialMediaManager", "AgentSpawn", "AgentEditor", "Memory", "Taskboard"
|
||||
];
|
||||
/// <summary>
|
||||
/// Die Namensliste steht in der Anwendungsschicht — die Weboberfläche braucht
|
||||
/// dieselbe, und zwei gepflegte Listen wären eine zu viel.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> KnownTools => AgentAdministration.KnownTools;
|
||||
|
||||
public AgentItemViewModel(AgentConfig config, IReadOnlyDictionary<string, string>? descriptions = null)
|
||||
{
|
||||
@@ -217,6 +218,18 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
{
|
||||
_logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Agents");
|
||||
ReloadAgents();
|
||||
|
||||
// Seit es eine zweite Oberflaeche gibt, ist diese Liste nicht mehr die
|
||||
// einzige Quelle von Aenderungen: Ein im Browser angelegter Agent stuende
|
||||
// sonst in der Datei und nicht im Fenster. Das Ereignis kommt auf dem Faden
|
||||
// des Verursachers — deshalb ueber den Dispatcher.
|
||||
//
|
||||
// Kein Abmelden: Diese Seite lebt so lange wie die Anwendung.
|
||||
host.InstanceChanged += change =>
|
||||
{
|
||||
if (change.Kind == InstanceChangeKind.Agents)
|
||||
Dispatcher.UIThread.Post(ReloadAgents);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -231,23 +244,34 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
/// Beschreibung eben leer.
|
||||
/// </summary>
|
||||
private IReadOnlyDictionary<string, string> ToolDescriptions()
|
||||
=> _host?.Tools.GetAll()
|
||||
.GroupBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First().Description, StringComparer.OrdinalIgnoreCase)
|
||||
?? new Dictionary<string, string>();
|
||||
=> _host?.AgentAdmin.ToolDescriptions() ?? new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Baut die Liste neu auf und behaelt die Auswahl, sofern es sie noch gibt.
|
||||
///
|
||||
/// <para>Die Auswahl zu behalten ist kein Feinschliff: Der Aufbau laeuft jetzt auch,
|
||||
/// wenn die Weboberflaeche etwas geaendert hat. Waere die Auswahl danach jedesmal
|
||||
/// weg, spraenge dem Benutzer der Cursor aus der Zeile, waehrend er arbeitet.</para>
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ReloadAgents()
|
||||
{
|
||||
if (_host is null)
|
||||
{
|
||||
Agents.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = SelectedAgent?.AgentId;
|
||||
|
||||
Agents.Clear();
|
||||
if (_host is null) return;
|
||||
|
||||
var descriptions = ToolDescriptions();
|
||||
|
||||
foreach (var agent in _host.Instance.Agents)
|
||||
Agents.Add(new AgentItemViewModel(agent, descriptions));
|
||||
|
||||
SelectedAgent = Agents.FirstOrDefault();
|
||||
SelectedAgent = Agents.FirstOrDefault(a => a.AgentId == previous) ?? Agents.FirstOrDefault();
|
||||
}
|
||||
|
||||
partial void OnSelectedAgentChanged(AgentItemViewModel? value)
|
||||
@@ -282,28 +306,22 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
var result = await RequestAddAgentDialog(vm);
|
||||
if (result is null) return;
|
||||
|
||||
if (_host.Instance.Agents.Any(a => a.AgentId.Equals(result.AgentId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
StatusText = $"Ein Agent mit der ID '{result.AgentId}' existiert bereits.";
|
||||
return;
|
||||
}
|
||||
// Erst die offenen Änderungen der Liste sichern, dann anlegen: Der Aufruf
|
||||
// schreibt die Instanzdatei, und was hier noch nicht übernommen war, ginge sonst
|
||||
// verloren.
|
||||
ApplyPendingEdits();
|
||||
|
||||
var newConfig = new AgentConfig
|
||||
{
|
||||
AgentId = result.AgentId,
|
||||
DisplayName = result.DisplayName,
|
||||
Model = result.Model,
|
||||
Description = result.Description
|
||||
};
|
||||
var outcome = _host.AgentAdmin.Add(
|
||||
new NewAgent(result.AgentId, result.DisplayName, result.Model, result.Description));
|
||||
|
||||
_host.Instance.Agents.Add(newConfig);
|
||||
SaveAgents();
|
||||
StatusText = outcome.Message;
|
||||
if (!outcome.Ok) return;
|
||||
|
||||
if (_host.AgentAdmin.Find(result.AgentId) is not { } newConfig) return;
|
||||
|
||||
var item = new AgentItemViewModel(newConfig, ToolDescriptions());
|
||||
Agents.Add(item);
|
||||
SelectedAgent = item;
|
||||
|
||||
StatusText = $"Agent '{result.DisplayName}' angelegt.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -311,14 +329,15 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
{
|
||||
if (_host is null || SelectedAgent is null) return;
|
||||
|
||||
var agentId = SelectedAgent.AgentId;
|
||||
_host.Instance.Agents.RemoveAll(a => a.AgentId == agentId);
|
||||
var removed = SelectedAgent;
|
||||
ApplyPendingEdits();
|
||||
|
||||
SaveAgents();
|
||||
Agents.Remove(SelectedAgent);
|
||||
var outcome = _host.AgentAdmin.Remove(removed.AgentId);
|
||||
StatusText = outcome.Message;
|
||||
if (!outcome.Ok) return;
|
||||
|
||||
Agents.Remove(removed);
|
||||
SelectedAgent = Agents.FirstOrDefault();
|
||||
|
||||
StatusText = $"Agent '{agentId}' entfernt.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -338,23 +357,19 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
}
|
||||
|
||||
var config = SelectedAgent.Config;
|
||||
var message = config.Scheduler?.TaskMessage ?? "Führe deine zugewiesenen Aufgaben aus.";
|
||||
|
||||
IsRunning = true;
|
||||
StatusText = $"Führe '{config.DisplayName}' aus…";
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _host.Engine.RunAsync(
|
||||
config, message, _host.Instance.InstanceId, CancellationToken.None);
|
||||
var outcome = await _host.Runs.RunAgentAsync(
|
||||
config.AgentId, config.Scheduler?.TaskMessage, CancellationToken.None);
|
||||
|
||||
StatusText = $"'{config.DisplayName}' abgeschlossen: {result.Status} "
|
||||
+ $"({result.StepCount} Schritte, {result.TokensUsed:N0} Tokens).";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Lauf fehlgeschlagen: {ex.Message}";
|
||||
_logger?.LogError(ex, "Handlauf des Agenten fehlgeschlagen");
|
||||
StatusText = outcome.Message;
|
||||
|
||||
if (!outcome.Ok)
|
||||
_logger?.LogWarning("Handlauf des Agenten fehlgeschlagen: {Grund}", outcome.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -420,19 +435,19 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
[RelayCommand]
|
||||
private void SaveToolSettings()
|
||||
{
|
||||
if (SelectedAgent is null || SelectedTool is null || ToolSettings is null) return;
|
||||
if (_host is null || SelectedAgent is null || SelectedTool is null || ToolSettings is null) return;
|
||||
|
||||
ToolSettings.ConfirmCommand.Execute(null);
|
||||
|
||||
if (ToolSettings.Result is not { } config) return;
|
||||
|
||||
SelectedAgent.Config.Tools[SelectedTool.Name] = config;
|
||||
|
||||
// Einstellungen zu schreiben heißt, das Werkzeug zu wollen.
|
||||
// Einstellungen zu schreiben heißt, das Werkzeug zu wollen — das Ankreuzfeld
|
||||
// zieht nach, damit Liste und Konfiguration nicht auseinanderlaufen.
|
||||
SelectedTool.IsEnabled = true;
|
||||
|
||||
SaveAgents();
|
||||
StatusText = $"Einstellungen für '{SelectedTool.Name}' gespeichert.";
|
||||
ApplyPendingEdits();
|
||||
StatusText = _host.AgentAdmin
|
||||
.SetToolSettings(SelectedAgent.AgentId, SelectedTool.Name, config).Message;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -440,14 +455,27 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
{
|
||||
if (_host is null) return;
|
||||
|
||||
foreach (var agentVm in Agents)
|
||||
agentVm.ApplyChanges();
|
||||
ApplyPendingEdits();
|
||||
_host.AgentAdmin.Save();
|
||||
|
||||
_host.Directories.SaveInstanceConfig(_host.InstancePath, _host.Instance);
|
||||
_logger?.LogInformation("Agenten-Konfiguration gespeichert.");
|
||||
StatusText = "Agenten-Konfiguration gespeichert.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Überträgt, was in den Feldern der Seite steht, in die Konfigurationsobjekte.
|
||||
///
|
||||
/// <para>Die Ansichtsmodelle halten eigene Eigenschaften und schreiben sie erst bei
|
||||
/// <c>ApplyChanges</c> zurück. Jeder Aufruf, der gleich die Instanzdatei schreibt,
|
||||
/// muss vorher hier durch — sonst verschwindet eine gerade getippte Änderung, weil
|
||||
/// jemand nebenbei einen Agenten angelegt hat.</para>
|
||||
/// </summary>
|
||||
private void ApplyPendingEdits()
|
||||
{
|
||||
foreach (var agentVm in Agents)
|
||||
agentVm.ApplyChanges();
|
||||
}
|
||||
|
||||
// ─── Teilerstellungen ───
|
||||
//
|
||||
// Die drei Teiler der Seite sollen den Neustart ueberleben. Gespeichert wird ein
|
||||
|
||||
Reference in New Issue
Block a user