UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4
Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag. Oberflaeche - Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml (eigene ControlThemes statt Fluent umzufaerben). - Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite statt eigenem Fenster, Texteditor-Fenster. - ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt. Rocket.Chat - Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird nur, wenn wirklich etwas anliegt. - send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist. - Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei Annahmen waren falsch und sind korrigiert. Deploymentcenter - DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json) erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke. - AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,7 @@ public sealed class AppHost : IAsyncDisposable
|
||||
registry.Register(new AgentEditorTool());
|
||||
registry.Register(new Tools.Memory.MemoryTool());
|
||||
registry.Register(new Tools.Taskboard.TaskboardTool());
|
||||
registry.Register(new Tools.RocketChat.RocketChatTool());
|
||||
|
||||
return registry;
|
||||
}
|
||||
@@ -468,8 +469,18 @@ public sealed class AppHost : IAsyncDisposable
|
||||
/// <summary>Version, Commit, Build-Datum und Kanal in einer Zeile — für Anzeigen.</summary>
|
||||
public static string BuildSummary => ReleaseInfo.Summary;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Beim Update fahren wir vor dem Beenden selbst herunter; danach ruft die
|
||||
// Oberflaeche Shutdown, und deren Behandlung raeumt ein zweites Mal auf. Ohne
|
||||
// diese Sperre gingen alle Schritte doppelt los — unter anderem ein zweiter
|
||||
// Abmeldevorgang beim Watchdog, der den gerade gesetzten Wartungszustand
|
||||
// wieder ueberschreibt.
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var step in _shutdown)
|
||||
{
|
||||
try { await step(); }
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<ProjectReference Include="..\ClawdDotNet.Tools.SocialMediaManager\ClawdDotNet.Tools.SocialMediaManager.csproj" />
|
||||
<ProjectReference Include="..\ClawdDotNet.Tools.Memory\ClawdDotNet.Tools.Memory.csproj" />
|
||||
<ProjectReference Include="..\ClawdDotNet.Tools.Taskboard\ClawdDotNet.Tools.Taskboard.csproj" />
|
||||
<ProjectReference Include="..\ClawdDotNet.Tools.RocketChat\ClawdDotNet.Tools.RocketChat.csproj" />
|
||||
<ProjectReference Include="..\ClawdDotNet.Tools.TelegramClient\ClawdDotNet.Tools.TelegramClient.csproj" />
|
||||
<!-- Deploymentcenter-SDK (Fremdrepo, netstandard2.0;net8.0). Liefert Hardware-ID v2,
|
||||
den verschluesselten Lizenz-Zwischenspeicher und die Update-Pruefung. Watchdog,
|
||||
|
||||
@@ -210,9 +210,30 @@ public sealed class DeploymentcenterService : IAsyncDisposable
|
||||
try
|
||||
{
|
||||
var client = new global::Deploymentcenter.Client.UpdateClient();
|
||||
|
||||
// Seit 2.4 liegt die Release-Ablage hinter HTTP-Basic-Auth; der Lizenzschlüssel
|
||||
// ist der Zugang. Die Anwendung kennt ihn ohnehin, es muss nichts zusätzlich
|
||||
// verteilt werden. Ohne ihn kommt ab dem Scharfschalten nur noch 401.
|
||||
//
|
||||
// platform bleibt offen: Das SDK setzt die Kennung des laufenden Systems ein.
|
||||
// Sie wegzulassen wäre keine Option — der Server liefert dann ausschließlich
|
||||
// plattformunabhängige Pakete.
|
||||
var result = await client.CheckForUpdateAsync(
|
||||
settings.DeploymentcenterUrl, LicenseInfo.ProductSlug, currentVersion,
|
||||
settings.UpdateChannel, cancellationToken: ct);
|
||||
settings.UpdateChannel,
|
||||
credentials: global::Deploymentcenter.Client.ReleaseCredentials
|
||||
.FromLicenseKey(settings.LicenseKey),
|
||||
cancellationToken: ct);
|
||||
|
||||
// Ein 401 ist kein Netzwerkfehler, sondern eine Aussage über die Lizenz.
|
||||
// Ohne die Unterscheidung sucht man bei einem abgelaufenen Vertrag am Netz.
|
||||
if (result.Unauthorized)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Update-Prüfung abgewiesen: {Message} – der Lizenzschlüssel öffnet die "
|
||||
+ "Release-Ablage nicht (mehr).", result.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Error is not null)
|
||||
{
|
||||
@@ -247,6 +268,78 @@ public sealed class DeploymentcenterService : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Meldet dem Watchdog, dass das Herunterfahren gleich ein Update ist und kein
|
||||
/// Ausfall. Muss <b>vor</b> dem Herunterfahren gerufen werden — die Meldung geht
|
||||
/// beim Beenden des Heartbeats raus.
|
||||
/// </summary>
|
||||
public void AnnounceUpdate(string version)
|
||||
=> _heartbeat?.AnnounceMaintenance($"Update auf {version} wird eingespielt.");
|
||||
|
||||
/// <summary>
|
||||
/// Startet den <c>update-agent</c> und übergibt ihm das Feld.
|
||||
///
|
||||
/// <para><b>Beendet die Anwendung bewusst nicht selbst.</b> <c>LaunchUpdateAgent</c>
|
||||
/// kann das (<c>exitCurrentApp</c>), täte es aber über
|
||||
/// <see cref="Environment.Exit"/> — und das übergeht jedes Aufräumen: Der Watchdog
|
||||
/// meldete nichts, Instanzdatenbank und Chatverläufe blieben auf halbem Weg stehen.
|
||||
/// Der Aufrufer fährt deshalb erst geordnet herunter und beendet danach selbst.
|
||||
/// Der Agent bekommt unsere Prozesskennung und wartet, bis wir wirklich weg
|
||||
/// sind.</para>
|
||||
/// </summary>
|
||||
/// <returns>Der Grund, wenn es nicht losging; sonst <c>null</c>.</returns>
|
||||
public string? StartUpdate(AppSettings settings, string currentVersion)
|
||||
{
|
||||
var agentPath = global::Deploymentcenter.Client.UpdateClient.ResolveAgentPath();
|
||||
|
||||
if (agentPath is null)
|
||||
{
|
||||
// Kein fest verdrahteter Name: Unter Linux trägt der Agent keine Endung.
|
||||
_logger.LogWarning(
|
||||
"Update angefordert, aber kein update-agent neben der Anwendung gefunden.");
|
||||
|
||||
return "Der update-agent wurde nicht gefunden. Er gehört neben die "
|
||||
+ "Programmdatei; das Installationspaket bringt ihn normalerweise mit.";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var started = global::Deploymentcenter.Client.UpdateClient.LaunchUpdateAgent(
|
||||
agentPath: agentPath,
|
||||
projectId: LicenseInfo.ProductSlug,
|
||||
channel: settings.UpdateChannel,
|
||||
action: "update",
|
||||
version: "latest",
|
||||
exitCurrentApp: false,
|
||||
currentVersion: currentVersion,
|
||||
waitForCurrentProcess: true,
|
||||
licenseKey: settings.LicenseKey,
|
||||
|
||||
// Ohne Signatur nicht installieren. Der SHA256 stammt aus derselben
|
||||
// Quelle wie das Paket — wer den Webroot oder die FTP-Zugangsdaten
|
||||
// kontrolliert, tauscht beide gemeinsam aus. Ausgerechnet auf dem Pfad,
|
||||
// der anschließend fremden Code ausführt.
|
||||
//
|
||||
// Tragfähig, weil ab 0.1.2 jedes Release signiert ist. Ein Rücksprung
|
||||
// auf 0.1.0 oder 0.1.1 bricht damit ab — das ist gewollt: Die beiden
|
||||
// stammen aus der Zeit vor dem Signierschlüssel.
|
||||
requireSignature: true);
|
||||
|
||||
if (!started)
|
||||
return "Der update-agent ließ sich nicht starten.";
|
||||
|
||||
_logger.LogInformation("update-agent gestartet ({Path}), Anwendung wird beendet.",
|
||||
agentPath);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "update-agent konnte nicht gestartet werden.");
|
||||
return $"Der update-agent konnte nicht gestartet werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_heartbeat is not null)
|
||||
|
||||
@@ -36,7 +36,12 @@ public sealed class OpenRouterStatusService : IAsyncDisposable
|
||||
/// <summary>Modelle, für die keine Preise vorliegen — werden in der Anzeige benannt.</summary>
|
||||
private readonly ConcurrentDictionary<string, byte> _modelsWithoutPricing = new();
|
||||
|
||||
private const double UsdToEur = 0.92;
|
||||
/// <summary>
|
||||
/// Grober Umrechnungskurs fuer die Anzeige. Oeffentlich, damit die Tokennutzungs-
|
||||
/// Seite denselben Wert verwendet — zwei Kopien eines veraltenden Kurses waeren
|
||||
/// zwei Gelegenheiten, verschiedene Betraege fuer dieselben Laeufe zu zeigen.
|
||||
/// </summary>
|
||||
public const double UsdToEur = 0.92;
|
||||
|
||||
public bool IsApiReachable { get; private set; }
|
||||
public string StatusText { get; private set; } = "Prüfe...";
|
||||
|
||||
@@ -52,6 +52,46 @@ public sealed class AppSettings
|
||||
[JsonPropertyName("logRefreshIntervalMs")]
|
||||
public int LogRefreshIntervalMs { get; set; } = 500;
|
||||
|
||||
// ─── Zustand der Oberflaeche ───
|
||||
//
|
||||
// Bewusst hier und nicht in der Instanzkonfiguration: Das sind Gewohnheiten des
|
||||
// Benutzers am Geraet, keine Eigenschaften der Instanz. Wer zwischen zwei Instanzen
|
||||
// wechselt, will nicht jedes Mal wieder im Hellmodus mit ausgeklappter Leiste landen.
|
||||
|
||||
[Category("UI")]
|
||||
[DisplayName("Farbschema")]
|
||||
[Description("Light oder Dark. Umschaltbar ueber das Symbol in der Kopfzeile.")]
|
||||
[JsonPropertyName("theme")]
|
||||
public string Theme { get; set; } = "Light";
|
||||
|
||||
[Category("UI")]
|
||||
[DisplayName("Seitenleiste eingeklappt")]
|
||||
[Description("Merkt sich, ob die Navigation auf Symbolbreite zusammengeklappt war.")]
|
||||
[JsonPropertyName("sidebarCollapsed")]
|
||||
public bool SidebarCollapsed { get; set; }
|
||||
|
||||
// Die drei Teiler der Agenten-Seite, jeweils als Anteil 0..1 der Gesamtflaeche.
|
||||
// Als Verhaeltnis und nicht in Pixeln: Sonst sitzt der Teiler nach einem Wechsel
|
||||
// auf einen anderen Bildschirm an einer voellig anderen Stelle.
|
||||
|
||||
[Category("UI")]
|
||||
[DisplayName("Agenten-Seite: Teiler waagerecht")]
|
||||
[Description("Anteil des oberen Bereichs (Agentenliste und Einstellungen), 0.2 bis 0.8.")]
|
||||
[JsonPropertyName("agentsSplitVertical")]
|
||||
public double AgentsSplitVertical { get; set; } = 0.55;
|
||||
|
||||
[Category("UI")]
|
||||
[DisplayName("Agenten-Seite: Teiler oben")]
|
||||
[Description("Anteil der Agentenliste an der Breite des oberen Bereichs, 0.2 bis 0.8.")]
|
||||
[JsonPropertyName("agentsSplitTopList")]
|
||||
public double AgentsSplitTopList { get; set; } = 0.44;
|
||||
|
||||
[Category("UI")]
|
||||
[DisplayName("Agenten-Seite: Teiler unten")]
|
||||
[Description("Anteil der Werkzeugliste an der Breite des unteren Bereichs, 0.2 bis 0.8.")]
|
||||
[JsonPropertyName("agentsSplitBottomList")]
|
||||
public double AgentsSplitBottomList { get; set; } = 0.44;
|
||||
|
||||
[Category("API")]
|
||||
[DisplayName("Status-Check-Intervall (Sek)")]
|
||||
[Description("Intervall in Sekunden für den OpenRouter-API-Status-Check.")]
|
||||
|
||||
Reference in New Issue
Block a user