Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
740649789e | ||
|
|
2853541629 |
@@ -1,124 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ClawdDotNet</RootNamespace>
|
||||
<ApplicationIcon>Martin-Berube-Flat-Animal-Crab.ico</ApplicationIcon>
|
||||
|
||||
<!-- ── Sauberes Build-Verzeichnis ── -->
|
||||
<!-- Nur deutsche + englische Satelliten-Assemblies (Sprachpakete) ausgeben -->
|
||||
<SatelliteResourceLanguages>de;en</SatelliteResourceLanguages>
|
||||
<!-- Keine XML-Dokumentationsdateien von NuGet-Paketen kopieren -->
|
||||
<PublishDocumentationFiles>false</PublishDocumentationFiles>
|
||||
<!-- PDB-Dateien nur im Debug-Build ausgeben -->
|
||||
<DebugSymbols Condition="'$(Configuration)' == 'Release'">false</DebugSymbols>
|
||||
<CopyOutputSymbolsToOutputDirectory Condition="'$(Configuration)' == 'Release'">false</CopyOutputSymbolsToOutputDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Unterordner mit eigenen Projekten aus dem Glob des Hauptprojekts nehmen.
|
||||
Ohne das kompiliert die WinForms-App die Test-Quellen mit. -->
|
||||
<ItemGroup>
|
||||
<Compile Remove="src\**" />
|
||||
<None Remove="src\**" />
|
||||
<EmbeddedResource Remove="src\**" />
|
||||
<Compile Remove="tests\**" />
|
||||
<None Remove="tests\**" />
|
||||
<EmbeddedResource Remove="tests\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3967.48" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Uebergang: SettingsManager und AppSettings sind nach ClawdDotNet.App gewandert.
|
||||
Das globale Using haelt die WinForms-Fassung baubar, solange sie waehrend der
|
||||
Avalonia-Portierung noch als Vorlage und Vergleich dient. Faellt mit dem Projekt
|
||||
weg (Aufgabe „WinForms-Oberflaeche entfernen"). -->
|
||||
<ItemGroup>
|
||||
<Using Include="ClawdDotNet.App.Settings" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="EmbeddedUI\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Martin-Berube-Flat-Animal-Crab.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="src\ClawdDotNet.App\ClawdDotNet.App.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Core\ClawdDotNet.Core.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.FileRW\ClawdDotNet.Tools.FileRW.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.Telegram\ClawdDotNet.Tools.Telegram.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.Mail\ClawdDotNet.Tools.Mail.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.Database\ClawdDotNet.Tools.Database.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.FTP\ClawdDotNet.Tools.FTP.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.DirectAPI\ClawdDotNet.Tools.DirectAPI.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.WebFetch\ClawdDotNet.Tools.WebFetch.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.WebMonitor\ClawdDotNet.Tools.WebMonitor.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.AgentComm\ClawdDotNet.Tools.AgentComm.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.AgentSpawn\ClawdDotNet.Tools.AgentSpawn.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.SocialMediaManager\ClawdDotNet.Tools.SocialMediaManager.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.AgentEditor\ClawdDotNet.Tools.AgentEditor.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.Memory\ClawdDotNet.Tools.Memory.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.Taskboard\ClawdDotNet.Tools.Taskboard.csproj" />
|
||||
<ProjectReference Include="src\ClawdDotNet.Tools.TelegramClient\ClawdDotNet.Tools.TelegramClient.csproj" />
|
||||
<!-- Der Verweis auf das LicenseLabrador-SDK ist entfallen: Lizenz, Watchdog, Updates,
|
||||
Fehler-Stream und Bugtracker laufen jetzt ueber das Deploymentcenter. Das SDK
|
||||
dafuer haengt an src\ClawdDotNet.App und kommt von dort transitiv mit. -->
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════
|
||||
Sauberes Programmverzeichnis
|
||||
═══════════════════════════════════════════════ -->
|
||||
|
||||
<!-- XML-Dokumentation von NuGet-Paketen nicht ins Build-Verzeichnis kopieren -->
|
||||
<Target Name="RemoveNuGetXmlDocs" AfterTargets="ResolveReferences">
|
||||
<ItemGroup>
|
||||
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
|
||||
Condition="'%(Extension)' == '.xml'" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- WebView2-WPF-Assemblies entfernen (nur WinForms wird verwendet) -->
|
||||
<Target Name="RemoveWebView2Wpf" AfterTargets="ResolveReferences">
|
||||
<ItemGroup>
|
||||
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
|
||||
Condition="$([System.String]::Copy('%(Filename)').Contains('WebView2.Wpf'))" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- Arbeitsordner (tools/, Logs/, Instances/) anlegen -->
|
||||
<Target Name="CreateWorkingDirectories" AfterTargets="Build">
|
||||
<MakeDir Directories="$(OutputPath)tools" />
|
||||
<MakeDir Directories="$(OutputPath)Logs" />
|
||||
<MakeDir Directories="$(OutputPath)Instances" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CreatePublishDirectories" AfterTargets="Publish">
|
||||
<MakeDir Directories="$(PublishDir)tools" />
|
||||
<MakeDir Directories="$(PublishDir)Logs" />
|
||||
<MakeDir Directories="$(PublishDir)Instances" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -24,15 +24,4 @@
|
||||
<Project Path="tests/ClawdDotNet.Core.Tests/ClawdDotNet.Core.Tests.csproj" />
|
||||
<Project Path="tests/ClawdDotNet.Tools.Tests/ClawdDotNet.Tools.Tests.csproj" />
|
||||
</Folder>
|
||||
<!--
|
||||
Die WinForms-Fassung (ClawdDotNet.csproj, frm_*.cs, UI/, Models/, EmbeddedUI/) ist
|
||||
seit dem Herausloesen der Anwendungsschicht nicht mehr Teil des Builds.
|
||||
|
||||
Die Dateien bleiben bis zum Abschluss der Portierung liegen: Sie sind die Vorlage
|
||||
fuer Aufbau und Verdrahtung der Avalonia-Ansichten — dafuer muessen sie lesbar sein,
|
||||
nicht uebersetzbar. Entfernt werden sie mit der Aufgabe
|
||||
„WinForms-Oberflaeche entfernen".
|
||||
|
||||
Oberflaeche ist jetzt src/ClawdDotNet.Desktop.
|
||||
-->
|
||||
</Solution>
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ClawdDotNet Deployment-Paket erstellen.
|
||||
Baut Release, packt alles Noetige in ein ZIP das per AnyDesk auf den Server kopiert wird.
|
||||
|
||||
.DESCRIPTION
|
||||
Zwei Modi:
|
||||
-Full Komplettes Deployment (alle Dateien, ~300 MB) — fuer Erstinstallation oder Dependency-Updates
|
||||
-Quick Nur ClawdDotNet-eigene Binaries (~2 MB) — fuer normale Code-Aenderungen (DEFAULT)
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-Build.ps1 # Quick-Deploy (nur eigene DLLs)
|
||||
.\Deploy-Build.ps1 -Full # Alles inkl. Dependencies
|
||||
.\Deploy-Build.ps1 -SkipBuild # ZIP ohne vorher zu bauen (wenn Build schon aktuell)
|
||||
#>
|
||||
param(
|
||||
[switch]$Full,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$IncludeAgentConfigs
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ─── Pfade ───
|
||||
$projectRoot = $PSScriptRoot
|
||||
$buildOutput = Join-Path $projectRoot "bin\Release\net10.0-windows"
|
||||
$deployDir = Join-Path $projectRoot "deploy"
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$mode = if ($Full) { "full" } else { "quick" }
|
||||
$zipName = "ClawdDotNet_${mode}_${timestamp}.zip"
|
||||
$zipPath = Join-Path $deployDir $zipName
|
||||
|
||||
# ─── 1. Build ───
|
||||
if (-not $SkipBuild) {
|
||||
Write-Host "`n=== Building Release ===" -ForegroundColor Cyan
|
||||
Push-Location $projectRoot
|
||||
dotnet build -c Release --no-restore
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "BUILD FAILED!" -ForegroundColor Red
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
Pop-Location
|
||||
Write-Host "Build OK" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "`n=== Build uebersprungen (SkipBuild) ===" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ─── 2. Staging-Ordner vorbereiten ───
|
||||
$stagingDir = Join-Path $deployDir "staging_$timestamp"
|
||||
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
|
||||
|
||||
if ($Full) {
|
||||
# ── Full Deploy: Alles ausser Instances/, Logs/, PDBs ──
|
||||
Write-Host "`n=== Full Deploy: Kopiere alle Dateien ===" -ForegroundColor Cyan
|
||||
|
||||
# Dateien im Root
|
||||
Get-ChildItem $buildOutput -File | Where-Object {
|
||||
$_.Extension -ne '.pdb'
|
||||
} | ForEach-Object {
|
||||
Copy-Item $_.FullName $stagingDir
|
||||
}
|
||||
|
||||
# Unterordner (ohne Instances und Logs)
|
||||
Get-ChildItem $buildOutput -Directory | Where-Object {
|
||||
$_.Name -notin @('Instances', 'Logs')
|
||||
} | ForEach-Object {
|
||||
Copy-Item $_.FullName (Join-Path $stagingDir $_.Name) -Recurse
|
||||
}
|
||||
|
||||
} else {
|
||||
# ── Quick Deploy: Nur ClawdDotNet-eigene Dateien ──
|
||||
Write-Host "`n=== Quick Deploy: Nur eigene Binaries ===" -ForegroundColor Cyan
|
||||
|
||||
Get-ChildItem $buildOutput -File | Where-Object {
|
||||
$_.Name -match '^ClawdDotNet\.' -and $_.Extension -ne '.pdb'
|
||||
} | ForEach-Object {
|
||||
Copy-Item $_.FullName $stagingDir
|
||||
}
|
||||
}
|
||||
|
||||
# ─── 2b. Optional: Agent-Configs mitkopieren ───
|
||||
if ($IncludeAgentConfigs) {
|
||||
Write-Host "=== Agent-Configs werden mitgepackt ===" -ForegroundColor Yellow
|
||||
$instancesSource = Join-Path $buildOutput "Instances"
|
||||
if (Test-Path $instancesSource) {
|
||||
$instancesDest = Join-Path $stagingDir "Instances"
|
||||
# Agent-Configs + Shared-Website-Templates kopieren
|
||||
Get-ChildItem $instancesSource -Recurse -File -Include "Identity.md","Soul.md","AgentSettings.json","AgentList.json","InstanceSettings.json","style.css","script.js" | ForEach-Object {
|
||||
$relPath = $_.FullName.Substring($instancesSource.Length)
|
||||
$destPath = Join-Path $instancesDest $relPath
|
||||
$destDir = Split-Path $destPath -Parent
|
||||
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
|
||||
Copy-Item $_.FullName $destPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── 3. Install-Skript beilegen ───
|
||||
$installScript = @'
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ClawdDotNet Update auf dem Server installieren.
|
||||
Stoppt die laufende Instanz, kopiert neue Dateien, startet neu.
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-Install.ps1 # Standard: ClawdDotNet.exe automatisch suchen
|
||||
.\Deploy-Install.ps1 -TargetDir "D:\ClawdDotNet" # Zielordner explizit angeben
|
||||
#>
|
||||
param(
|
||||
[string]$TargetDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Zielordner bestimmen
|
||||
if (-not $TargetDir) {
|
||||
$candidates = @(
|
||||
"C:\ClawdDotNet",
|
||||
"D:\ClawdDotNet",
|
||||
"$env:ProgramFiles\ClawdDotNet",
|
||||
"$env:LOCALAPPDATA\ClawdDotNet"
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path (Join-Path $c "ClawdDotNet.exe")) {
|
||||
$TargetDir = $c
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $TargetDir) {
|
||||
Write-Host "ClawdDotNet-Installation nicht gefunden!" -ForegroundColor Red
|
||||
Write-Host 'Bitte mit -TargetDir angeben, z.B.:'
|
||||
Write-Host ' .\Deploy-Install.ps1 -TargetDir "D:\ClawdDotNet"'
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== ClawdDotNet Update ===" -ForegroundColor Cyan
|
||||
Write-Host "Ziel: $TargetDir"
|
||||
|
||||
# 1. Prozess stoppen
|
||||
$proc = Get-Process -Name "ClawdDotNet" -ErrorAction SilentlyContinue
|
||||
if ($proc) {
|
||||
Write-Host "Stoppe ClawdDotNet..." -ForegroundColor Yellow
|
||||
$proc | Stop-Process -Force -Confirm:$false
|
||||
Start-Sleep -Seconds 2
|
||||
$timeout = 15
|
||||
while ((Get-Process -Name "ClawdDotNet" -ErrorAction SilentlyContinue) -and $timeout -gt 0) {
|
||||
Start-Sleep -Seconds 1
|
||||
$timeout--
|
||||
}
|
||||
if ($timeout -eq 0) {
|
||||
Write-Host "WARNUNG: Prozess konnte nicht gestoppt werden!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Gestoppt." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "ClawdDotNet laeuft nicht - kein Stopp noetig." -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# 2. Backup erstellen (nur eigene DLLs)
|
||||
$backupDir = Join-Path $TargetDir ("_backup_" + (Get-Date -Format "yyyyMMdd_HHmmss"))
|
||||
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
|
||||
Get-ChildItem $TargetDir -File | Where-Object { $_.Name -match '^ClawdDotNet\.' } | ForEach-Object {
|
||||
Copy-Item $_.FullName $backupDir
|
||||
}
|
||||
Write-Host "Backup erstellt: $backupDir" -ForegroundColor Gray
|
||||
|
||||
# 3. Neue Dateien kopieren
|
||||
$sourceDir = $PSScriptRoot
|
||||
$sourceFull = (Resolve-Path $sourceDir).Path.TrimEnd('\')
|
||||
$targetFull = (Resolve-Path $TargetDir).Path.TrimEnd('\')
|
||||
|
||||
if ($sourceFull -eq $targetFull) {
|
||||
# ZIP wurde direkt im Zielordner entpackt - Dateien sind schon da
|
||||
Write-Host "ZIP wurde direkt im Zielordner entpackt - Dateien bereits vorhanden." -ForegroundColor Yellow
|
||||
$fileCount = (Get-ChildItem $sourceDir -File | Where-Object { $_.Name -notin @('Deploy-Install.ps1') }).Count
|
||||
} else {
|
||||
$fileCount = 0
|
||||
Get-ChildItem $sourceDir -File | Where-Object { $_.Name -notin @('Deploy-Install.ps1') } | ForEach-Object {
|
||||
Copy-Item $_.FullName $TargetDir -Force
|
||||
$fileCount++
|
||||
}
|
||||
|
||||
# Unterordner kopieren (falls vorhanden, z.B. bei Full-Deploy)
|
||||
Get-ChildItem $sourceDir -Directory | Where-Object { $_.Name -notmatch '^_backup' } | ForEach-Object {
|
||||
$destSubDir = Join-Path $TargetDir $_.Name
|
||||
Copy-Item $_.FullName $destSubDir -Recurse -Force
|
||||
$fileCount += (Get-ChildItem $_.FullName -Recurse -File).Count
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "$fileCount Dateien aktualisiert." -ForegroundColor Green
|
||||
|
||||
# 4. Neu starten
|
||||
Write-Host "Starte ClawdDotNet..." -ForegroundColor Cyan
|
||||
$exePath = Join-Path $TargetDir "ClawdDotNet.exe"
|
||||
Start-Process $exePath -WorkingDirectory $TargetDir
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
if (Get-Process -Name "ClawdDotNet" -ErrorAction SilentlyContinue) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Update erfolgreich! ClawdDotNet laeuft. ===" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host "=== WARNUNG: Prozess nicht gefunden. Bitte manuell pruefen. ===" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# 5. Alte Backups aufraeumen (behalte die letzten 5)
|
||||
$oldBackups = Get-ChildItem $TargetDir -Directory | Where-Object { $_.Name -match '^_backup_' } | Sort-Object Name -Descending | Select-Object -Skip 5
|
||||
foreach ($old in $oldBackups) {
|
||||
Remove-Item $old.FullName -Recurse -Force
|
||||
Write-Host "Altes Backup entfernt: $($old.Name)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Fertig." -ForegroundColor Green
|
||||
'@
|
||||
|
||||
$installScriptPath = Join-Path $stagingDir "Deploy-Install.ps1"
|
||||
# UTF-8 OHNE BOM - wichtig fuer PowerShell 5.1 auf Windows Server
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($installScriptPath, $installScript, $utf8NoBom)
|
||||
|
||||
# ─── 4. ZIP erstellen ───
|
||||
if (-not (Test-Path $deployDir)) { New-Item -ItemType Directory -Path $deployDir -Force | Out-Null }
|
||||
|
||||
Write-Host "`n=== Erstelle ZIP: $zipName ===" -ForegroundColor Cyan
|
||||
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
|
||||
Compress-Archive -Path "$stagingDir\*" -DestinationPath $zipPath -CompressionLevel Optimal
|
||||
|
||||
# Staging aufraeumen
|
||||
Remove-Item $stagingDir -Recurse -Force
|
||||
|
||||
# ─── 5. Zusammenfassung ───
|
||||
$zipSize = (Get-Item $zipPath).Length
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host " Deployment-Paket erstellt!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host " Modus: $( if ($Full) { 'FULL (alle Dateien)' } else { 'QUICK (nur eigene DLLs)' } )"
|
||||
Write-Host " Datei: $zipPath"
|
||||
Write-Host " Groesse: $([math]::Round($zipSize/1KB, 0)) KB ($([math]::Round($zipSize/1MB, 1)) MB)"
|
||||
Write-Host ""
|
||||
Write-Host " Naechste Schritte:" -ForegroundColor Yellow
|
||||
Write-Host " 1. ZIP per AnyDesk auf den Server kopieren"
|
||||
Write-Host " 2. Auf dem Server entpacken"
|
||||
Write-Host " 3. Deploy-Install.ps1 als Admin ausfuehren"
|
||||
Write-Host ""
|
||||
|
||||
# ZIP-Ordner im Explorer oeffnen
|
||||
Start-Process "explorer.exe" "/select,`"$zipPath`""
|
||||
@@ -1,9 +0,0 @@
|
||||
window.__bridge = {
|
||||
receive(msg) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('bridge:' + msg.type, { detail: msg }));
|
||||
},
|
||||
send(msg) {
|
||||
window.chrome.webview.postMessage(JSON.stringify(msg));
|
||||
}
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg-dark: #1e1e1e;
|
||||
--bg-sidebar: #252526;
|
||||
--bg-input: #2d2d2d;
|
||||
--bg-bubble-user: #264f78;
|
||||
--bg-bubble-agent: #333333;
|
||||
--text: #cccccc;
|
||||
--text-bright: #e0e0e0;
|
||||
--text-dim: #888888;
|
||||
--accent: #569cd6;
|
||||
--border: #3e3e3e;
|
||||
--hover: #2a2d2e;
|
||||
}
|
||||
|
||||
html, body { height: 100%; font-family: 'Segoe UI', sans-serif; background: var(--bg-dark); color: var(--text); }
|
||||
|
||||
#app { display: flex; flex-direction: column; height: 100%; }
|
||||
|
||||
#chat-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 20px; border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-sidebar);
|
||||
}
|
||||
#chat-agent-name { font-size: 15px; font-weight: 600; color: var(--text-bright); }
|
||||
#chat-header-actions { display: flex; gap: 8px; }
|
||||
#chat-header-actions button {
|
||||
background: transparent; border: 1px solid var(--border); color: var(--text);
|
||||
padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;
|
||||
}
|
||||
#chat-header-actions button:hover { background: var(--hover); border-color: var(--accent); }
|
||||
|
||||
#chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
.chat-bubble {
|
||||
max-width: 75%; padding: 10px 14px; border-radius: 10px;
|
||||
font-size: 13px; line-height: 1.5; word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.chat-bubble.user { background: var(--bg-bubble-user); color: var(--text-bright); align-self: flex-end; border-bottom-right-radius: 2px; }
|
||||
.chat-bubble.assistant { background: var(--bg-bubble-agent); color: var(--text); align-self: flex-start; border-bottom-left-radius: 2px; }
|
||||
.chat-bubble .timestamp { display: block; font-size: 10px; color: var(--text-dim); margin-top: 4px; }
|
||||
|
||||
.typing-indicator { align-self: flex-start; padding: 10px 14px; background: var(--bg-bubble-agent); border-radius: 10px; }
|
||||
.typing-indicator span { display: inline-block; width: 6px; height: 6px; background: var(--text-dim); border-radius: 50%; margin: 0 2px; animation: typing 1.4s infinite; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes typing { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-4px); } }
|
||||
|
||||
#chat-input-area {
|
||||
display: flex; align-items: flex-end; gap: 8px;
|
||||
padding: 12px 20px; border-top: 1px solid var(--border);
|
||||
background: var(--bg-sidebar);
|
||||
}
|
||||
#chat-input {
|
||||
flex: 1; resize: none; border: 1px solid var(--border); border-radius: 6px;
|
||||
background: var(--bg-input); color: var(--text-bright); padding: 10px 12px;
|
||||
font-family: inherit; font-size: 13px; line-height: 1.4;
|
||||
max-height: 120px; outline: none;
|
||||
}
|
||||
#chat-input:focus { border-color: var(--accent); }
|
||||
#btn-send {
|
||||
background: var(--accent); color: #fff; border: none; border-radius: 6px;
|
||||
padding: 10px 18px; cursor: pointer; font-size: 13px; font-weight: 500;
|
||||
}
|
||||
#btn-send:hover { opacity: 0.85; }
|
||||
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||
@@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ClawdDotNet – Chat</title>
|
||||
<link rel="stylesheet" href="chat.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="chat-header">
|
||||
<span id="chat-agent-name">Agent</span>
|
||||
<div id="chat-header-actions">
|
||||
<button id="btn-run-now" title="Jetzt ausführen">▶</button>
|
||||
<button id="btn-abort" title="Abbrechen" style="display:none;">■</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-messages"></div>
|
||||
<div id="chat-input-area">
|
||||
<textarea id="chat-input" placeholder="Nachricht eingeben..." rows="1"></textarea>
|
||||
<button id="btn-send">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="bridge.js"></script>
|
||||
<script src="chat.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,135 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const agentId = new URLSearchParams(location.search).get('agent');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const btnSend = document.getElementById('btn-send');
|
||||
const chatAgentName = document.getElementById('chat-agent-name');
|
||||
const btnRunNow = document.getElementById('btn-run-now');
|
||||
const btnAbort = document.getElementById('btn-abort');
|
||||
|
||||
// ─── Bridge Events ───
|
||||
|
||||
document.addEventListener('bridge:agent_list_update', e => {
|
||||
const data = e.detail.extra;
|
||||
const list = Array.isArray(data) ? data : (data?.agents ?? []);
|
||||
const agent = list.find(a => a.agentId === agentId);
|
||||
if (agent) chatAgentName.textContent = agent.displayName;
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:chat_history', e => {
|
||||
if (e.detail.agentId !== agentId) return;
|
||||
chatMessages.innerHTML = '';
|
||||
const history = e.detail.extra;
|
||||
if (Array.isArray(history)) {
|
||||
history.forEach(entry => appendBubble(entry.role, entry.content, entry.timestamp));
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:chat_message', e => {
|
||||
if (e.detail.agentId !== agentId) return;
|
||||
removeTypingIndicator();
|
||||
const extra = e.detail.extra ?? {};
|
||||
appendBubble(extra.role ?? 'assistant', e.detail.content, extra.timestamp);
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:chat_typing', e => {
|
||||
if (e.detail.agentId !== agentId) return;
|
||||
showTypingIndicator();
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:run_started', e => {
|
||||
if (e.detail.agentId !== agentId) return;
|
||||
btnAbort.style.display = 'inline-block';
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:run_finished', e => {
|
||||
if (e.detail.agentId !== agentId) return;
|
||||
btnAbort.style.display = 'none';
|
||||
removeTypingIndicator();
|
||||
});
|
||||
|
||||
// ─── Chat Bubbles ───
|
||||
|
||||
function appendBubble(role, content, timestamp) {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'chat-bubble ' + (role === 'user' ? 'user' : 'assistant');
|
||||
bubble.textContent = content || '';
|
||||
|
||||
if (timestamp) {
|
||||
const ts = document.createElement('span');
|
||||
ts.className = 'timestamp';
|
||||
ts.textContent = formatTime(timestamp);
|
||||
bubble.appendChild(ts);
|
||||
}
|
||||
|
||||
chatMessages.appendChild(bubble);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function showTypingIndicator() {
|
||||
removeTypingIndicator();
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'typing-indicator';
|
||||
indicator.id = 'typing';
|
||||
indicator.innerHTML = '<span></span><span></span><span></span>';
|
||||
chatMessages.appendChild(indicator);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function removeTypingIndicator() {
|
||||
document.getElementById('typing')?.remove();
|
||||
}
|
||||
|
||||
// ─── Input ───
|
||||
|
||||
btnSend.addEventListener('click', sendMessage);
|
||||
|
||||
chatInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
chatInput.addEventListener('input', () => {
|
||||
chatInput.style.height = 'auto';
|
||||
chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const text = chatInput.value.trim();
|
||||
if (!text || !agentId) return;
|
||||
|
||||
chatInput.value = '';
|
||||
chatInput.style.height = 'auto';
|
||||
|
||||
window.__bridge.send({
|
||||
type: 'user_message',
|
||||
agentId: agentId,
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Header Buttons ───
|
||||
|
||||
btnRunNow.addEventListener('click', () => {
|
||||
if (!agentId) return;
|
||||
window.__bridge.send({ type: 'run_now', agentId: agentId });
|
||||
});
|
||||
|
||||
btnAbort.addEventListener('click', () => {
|
||||
if (!agentId) return;
|
||||
window.__bridge.send({ type: 'abort_run', agentId: agentId });
|
||||
});
|
||||
|
||||
// ─── Helpers ───
|
||||
|
||||
function formatTime(ts) {
|
||||
try {
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
} catch { return ''; }
|
||||
}
|
||||
})();
|
||||
@@ -1,117 +0,0 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg-dark: #1e1e1e;
|
||||
--bg-sidebar: #252526;
|
||||
--bg-chat: #1e1e1e;
|
||||
--bg-input: #2d2d2d;
|
||||
--bg-bubble-user: #264f78;
|
||||
--bg-bubble-agent: #333333;
|
||||
--text: #cccccc;
|
||||
--text-bright: #e0e0e0;
|
||||
--text-dim: #888888;
|
||||
--accent: #569cd6;
|
||||
--border: #3e3e3e;
|
||||
--hover: #2a2d2e;
|
||||
--agent-active: #37373d;
|
||||
--status-running: #4ec9b0;
|
||||
--status-idle: #4ec94e;
|
||||
--status-offline: #888888;
|
||||
--status-error: #f44747;
|
||||
}
|
||||
|
||||
html, body { height: 100%; font-family: 'Segoe UI', sans-serif; background: var(--bg-dark); color: var(--text); }
|
||||
|
||||
#app { display: flex; height: 100%; }
|
||||
|
||||
/* ─── Sidebar ─── */
|
||||
#sidebar { width: 280px; min-width: 220px; background: var(--bg-sidebar); border-right: 1px solid var(--border); display: flex; flex-direction: column; }
|
||||
#sidebar-header { padding: 16px; border-bottom: 1px solid var(--border); }
|
||||
#sidebar-header h2 { font-size: 14px; font-weight: 600; color: var(--text-bright); text-transform: uppercase; letter-spacing: 1px; }
|
||||
#agent-list { flex: 1; overflow-y: auto; padding: 8px; }
|
||||
|
||||
.agent-item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; margin-bottom: 4px; border-radius: 6px;
|
||||
cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.agent-item:hover { background: var(--hover); }
|
||||
.agent-item.active { background: var(--agent-active); border-left: 3px solid var(--accent); }
|
||||
|
||||
.agent-status-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
||||
background: var(--status-idle);
|
||||
}
|
||||
.agent-status-dot.running { background: var(--status-running); animation: pulse 1.5s infinite; }
|
||||
.agent-status-dot.offline { background: var(--status-offline); }
|
||||
.agent-status-dot.error { background: var(--status-error); }
|
||||
|
||||
.agent-info { flex: 1; min-width: 0; }
|
||||
.agent-name { font-size: 13px; font-weight: 500; color: var(--text-bright); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.agent-model { font-size: 11px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
|
||||
/* ─── Chat Area ─── */
|
||||
#chat-area { flex: 1; display: flex; flex-direction: column; background: var(--bg-chat); }
|
||||
|
||||
#chat-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 20px; border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-sidebar);
|
||||
}
|
||||
#chat-agent-name { font-size: 15px; font-weight: 600; color: var(--text-bright); }
|
||||
#chat-header-actions { display: flex; gap: 8px; }
|
||||
#chat-header-actions button {
|
||||
background: transparent; border: 1px solid var(--border); color: var(--text);
|
||||
padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;
|
||||
}
|
||||
#chat-header-actions button:hover { background: var(--hover); border-color: var(--accent); }
|
||||
|
||||
#chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
#empty-state { display: flex; align-items: center; justify-content: center; height: 100%; }
|
||||
#empty-state p { color: var(--text-dim); font-size: 14px; }
|
||||
|
||||
.chat-bubble {
|
||||
max-width: 75%; padding: 10px 14px; border-radius: 10px;
|
||||
font-size: 13px; line-height: 1.5; word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.chat-bubble.user { background: var(--bg-bubble-user); color: var(--text-bright); align-self: flex-end; border-bottom-right-radius: 2px; }
|
||||
.chat-bubble.assistant { background: var(--bg-bubble-agent); color: var(--text); align-self: flex-start; border-bottom-left-radius: 2px; }
|
||||
|
||||
.chat-bubble .timestamp { display: block; font-size: 10px; color: var(--text-dim); margin-top: 4px; }
|
||||
|
||||
.typing-indicator { align-self: flex-start; padding: 10px 14px; background: var(--bg-bubble-agent); border-radius: 10px; }
|
||||
.typing-indicator span { display: inline-block; width: 6px; height: 6px; background: var(--text-dim); border-radius: 50%; margin: 0 2px; animation: typing 1.4s infinite; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes typing { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-4px); } }
|
||||
|
||||
/* ─── Input ─── */
|
||||
#chat-input-area {
|
||||
display: flex; align-items: flex-end; gap: 8px;
|
||||
padding: 12px 20px; border-top: 1px solid var(--border);
|
||||
background: var(--bg-sidebar);
|
||||
}
|
||||
#chat-input {
|
||||
flex: 1; resize: none; border: 1px solid var(--border); border-radius: 6px;
|
||||
background: var(--bg-input); color: var(--text-bright); padding: 10px 12px;
|
||||
font-family: inherit; font-size: 13px; line-height: 1.4;
|
||||
max-height: 120px; outline: none;
|
||||
}
|
||||
#chat-input:focus { border-color: var(--accent); }
|
||||
#btn-send {
|
||||
background: var(--accent); color: #fff; border: none; border-radius: 6px;
|
||||
padding: 10px 18px; cursor: pointer; font-size: 13px; font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#btn-send:hover { opacity: 0.85; }
|
||||
#btn-send:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* ─── Scrollbar ─── */
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||
@@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ClawdDotNet – Agent Chat</title>
|
||||
<link rel="stylesheet" href="overview.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<aside id="sidebar">
|
||||
<div id="sidebar-header">
|
||||
<h2>Agenten</h2>
|
||||
</div>
|
||||
<div id="agent-list"></div>
|
||||
</aside>
|
||||
<main id="chat-area">
|
||||
<div id="chat-header">
|
||||
<span id="chat-agent-name">Wähle einen Agenten</span>
|
||||
<div id="chat-header-actions">
|
||||
<button id="btn-open-window" title="In eigenem Fenster öffnen" style="display:none;">↗</button>
|
||||
<button id="btn-run-now" title="Jetzt ausführen" style="display:none;">▶</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-messages">
|
||||
<div id="empty-state">
|
||||
<p>Wähle einen Agenten aus der Liste, um den Chat zu starten.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-input-area" style="display:none;">
|
||||
<textarea id="chat-input" placeholder="Nachricht eingeben..." rows="1"></textarea>
|
||||
<button id="btn-send">Senden</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="bridge.js"></script>
|
||||
<script src="overview.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,191 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
let selectedAgentId = null;
|
||||
let agents = [];
|
||||
|
||||
const agentList = document.getElementById('agent-list');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const btnSend = document.getElementById('btn-send');
|
||||
const chatAgentName = document.getElementById('chat-agent-name');
|
||||
const chatInputArea = document.getElementById('chat-input-area');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const btnOpenWindow = document.getElementById('btn-open-window');
|
||||
const btnRunNow = document.getElementById('btn-run-now');
|
||||
|
||||
// ─── Bridge Events ───
|
||||
|
||||
document.addEventListener('bridge:agent_list_update', e => {
|
||||
const data = e.detail.extra;
|
||||
agents = Array.isArray(data) ? data : (data?.agents ?? []);
|
||||
renderSidebar();
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:agent_status', e => {
|
||||
const d = e.detail;
|
||||
const agent = agents.find(a => a.agentId === d.agentId);
|
||||
if (agent) {
|
||||
agent.status = d.status;
|
||||
renderSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:chat_history', e => {
|
||||
if (e.detail.agentId !== selectedAgentId) return;
|
||||
const history = e.detail.extra;
|
||||
chatMessages.innerHTML = '';
|
||||
if (Array.isArray(history)) {
|
||||
history.forEach(entry => appendBubble(entry.role, entry.content, entry.timestamp));
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:chat_message', e => {
|
||||
if (e.detail.agentId !== selectedAgentId) return;
|
||||
removeTypingIndicator();
|
||||
const extra = e.detail.extra ?? {};
|
||||
appendBubble(extra.role ?? 'assistant', e.detail.content, extra.timestamp);
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:chat_typing', e => {
|
||||
if (e.detail.agentId !== selectedAgentId) return;
|
||||
showTypingIndicator();
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:run_started', e => {
|
||||
const agent = agents.find(a => a.agentId === e.detail.agentId);
|
||||
if (agent) { agent.status = 'running'; renderSidebar(); }
|
||||
});
|
||||
|
||||
document.addEventListener('bridge:run_finished', e => {
|
||||
const agent = agents.find(a => a.agentId === e.detail.agentId);
|
||||
if (agent) { agent.status = 'idle'; renderSidebar(); }
|
||||
removeTypingIndicator();
|
||||
});
|
||||
|
||||
// ─── Sidebar ───
|
||||
|
||||
function renderSidebar() {
|
||||
agentList.innerHTML = '';
|
||||
agents.forEach(agent => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'agent-item' + (agent.agentId === selectedAgentId ? ' active' : '');
|
||||
item.innerHTML = `
|
||||
<div class="agent-status-dot ${agent.status || 'idle'}"></div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">${escapeHtml(agent.displayName)}</div>
|
||||
<div class="agent-model">${escapeHtml(agent.model || '')}</div>
|
||||
</div>`;
|
||||
item.addEventListener('click', () => selectAgent(agent.agentId));
|
||||
agentList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function selectAgent(agentId) {
|
||||
selectedAgentId = agentId;
|
||||
const agent = agents.find(a => a.agentId === agentId);
|
||||
|
||||
chatAgentName.textContent = agent ? agent.displayName : agentId;
|
||||
chatInputArea.style.display = 'flex';
|
||||
emptyState?.remove();
|
||||
btnOpenWindow.style.display = 'inline-block';
|
||||
btnRunNow.style.display = 'inline-block';
|
||||
chatMessages.innerHTML = '';
|
||||
|
||||
renderSidebar();
|
||||
|
||||
window.__bridge.send({
|
||||
type: 'select_agent',
|
||||
agentId: agentId
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Chat Bubbles ───
|
||||
|
||||
function appendBubble(role, content, timestamp) {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'chat-bubble ' + (role === 'user' ? 'user' : 'assistant');
|
||||
bubble.textContent = content || '';
|
||||
|
||||
if (timestamp) {
|
||||
const ts = document.createElement('span');
|
||||
ts.className = 'timestamp';
|
||||
ts.textContent = formatTime(timestamp);
|
||||
bubble.appendChild(ts);
|
||||
}
|
||||
|
||||
chatMessages.appendChild(bubble);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function showTypingIndicator() {
|
||||
removeTypingIndicator();
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'typing-indicator';
|
||||
indicator.id = 'typing';
|
||||
indicator.innerHTML = '<span></span><span></span><span></span>';
|
||||
chatMessages.appendChild(indicator);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function removeTypingIndicator() {
|
||||
document.getElementById('typing')?.remove();
|
||||
}
|
||||
|
||||
// ─── Input Handling ───
|
||||
|
||||
btnSend.addEventListener('click', sendMessage);
|
||||
|
||||
chatInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
chatInput.addEventListener('input', () => {
|
||||
chatInput.style.height = 'auto';
|
||||
chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const text = chatInput.value.trim();
|
||||
if (!text || !selectedAgentId) return;
|
||||
|
||||
chatInput.value = '';
|
||||
chatInput.style.height = 'auto';
|
||||
|
||||
window.__bridge.send({
|
||||
type: 'user_message',
|
||||
agentId: selectedAgentId,
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Header Buttons ───
|
||||
|
||||
btnOpenWindow.addEventListener('click', () => {
|
||||
if (!selectedAgentId) return;
|
||||
window.__bridge.send({ type: 'open_agent_chat', agentId: selectedAgentId });
|
||||
});
|
||||
|
||||
btnRunNow.addEventListener('click', () => {
|
||||
if (!selectedAgentId) return;
|
||||
window.__bridge.send({ type: 'run_now', agentId: selectedAgentId });
|
||||
});
|
||||
|
||||
// ─── Helpers ───
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
try {
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
} catch { return ''; }
|
||||
}
|
||||
})();
|
||||
@@ -1,8 +0,0 @@
|
||||
Agenten sollen den Hinweis bekommen, das sie mich informieren sollen, wenn sie der Meinung sind das ssie ein weiteres Tool benötigen, das ihnen derzeit nicht zur Verfügung steht.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
SharedWorkspace für die gesamte Instanz hinzufügen ?
|
||||
Ein Ort im Dateisystem, wo sie gemeinsam an verschiedenen Dateien / Projekten arbeiten können?
|
||||
|
Before Width: | Height: | Size: 173 KiB |
@@ -1,200 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using ClawdDotNet.Core.Config;
|
||||
|
||||
namespace ClawdDotNet.Models;
|
||||
|
||||
/// <summary>Bietet die drei gültigen Prompt-Caching-Werte als Auswahlliste an.</summary>
|
||||
public sealed class PromptCachingConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
|
||||
=> new(new[] { "auto", "on", "off" });
|
||||
}
|
||||
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
public sealed class AgentSettingsViewModel
|
||||
{
|
||||
private readonly AgentConfig _config;
|
||||
|
||||
public AgentSettingsViewModel(AgentConfig config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
// ──────────────── Identity ────────────────
|
||||
|
||||
[Category("1 - Identity")]
|
||||
[DisplayName("Agent-ID")]
|
||||
[Description("Eindeutige ID des Agenten. Wird intern und in Logs verwendet.")]
|
||||
[ReadOnly(true)]
|
||||
public string AgentId => _config.AgentId;
|
||||
|
||||
[Category("1 - Identity")]
|
||||
[DisplayName("Anzeigename")]
|
||||
[Description("Freundlicher Name des Agenten (z.B. 'Marktanalyst').")]
|
||||
public string DisplayName
|
||||
{
|
||||
get => _config.DisplayName;
|
||||
set => _config.DisplayName = value;
|
||||
}
|
||||
|
||||
[Category("1 - Identity")]
|
||||
[DisplayName("Modell")]
|
||||
[Description("LLM-Modell das dieser Agent verwendet. Dropdown zeigt verfügbare Modelle von OpenRouter.")]
|
||||
[TypeConverter(typeof(ModelTypeConverter))]
|
||||
public string Model
|
||||
{
|
||||
get => _config.Model;
|
||||
set => _config.Model = value;
|
||||
}
|
||||
|
||||
[Category("1 - Identity")]
|
||||
[DisplayName("Identity")]
|
||||
[Description("Aus Identity.md geladen – definiert WER der Agent ist. Bearbeitung über den Toolbar-Button 'Identity bearbeiten'.")]
|
||||
[ReadOnly(true)]
|
||||
public string IdentityStatus =>
|
||||
string.IsNullOrWhiteSpace(_config.Identity) ? "(nicht definiert)" : $"✔ {_config.Identity.Split('\n').Length} Zeilen";
|
||||
|
||||
[Category("1 - Identity")]
|
||||
[DisplayName("Soul")]
|
||||
[Description("Aus Soul.md geladen – definiert WIE der Agent denkt. Bearbeitung über den Toolbar-Button 'Soul bearbeiten'.")]
|
||||
[ReadOnly(true)]
|
||||
public string SoulStatus =>
|
||||
string.IsNullOrWhiteSpace(_config.Soul) ? "(nicht definiert)" : $"✔ {_config.Soul.Split('\n').Length} Zeilen";
|
||||
|
||||
// ──────────────── Loop-Schutz ────────────────
|
||||
|
||||
[Category("2 - Loop-Schutz")]
|
||||
[DisplayName("Max. Schritte pro Run")]
|
||||
[Description("Maximale Anzahl LLM-Aufrufe pro Run. Verhindert Endlosschleifen bei fehlerhaften Tool-Calls.")]
|
||||
public int MaxSteps
|
||||
{
|
||||
get => _config.LoopGuard.MaxSteps;
|
||||
set => _config.LoopGuard.MaxSteps = Math.Max(1, value);
|
||||
}
|
||||
|
||||
[Category("2 - Loop-Schutz")]
|
||||
[DisplayName("Kostenbudget pro Run (Tokens)")]
|
||||
[Description("Summe ALLER abgerechneten Tokens eines Runs über alle Schritte. " +
|
||||
"Da jeder Schritt den kompletten Kontext erneut sendet, wächst diese Summe " +
|
||||
"überproportional — der Wert liegt deutlich über der Kontextgröße. " +
|
||||
"Für die Kontextgröße ist 'Max. Kontext-Tokens' zuständig.")]
|
||||
public int MaxCumulativeTokens
|
||||
{
|
||||
get => _config.LoopGuard.MaxCumulativeTokens;
|
||||
set => _config.LoopGuard.MaxCumulativeTokens = Math.Max(10_000, value);
|
||||
}
|
||||
|
||||
[Category("2 - Loop-Schutz")]
|
||||
[DisplayName("Timeout (Sekunden)")]
|
||||
[Description("Maximale Laufzeit pro Run in Sekunden. Danach wird der Run abgebrochen.")]
|
||||
public int TimeoutSeconds
|
||||
{
|
||||
get => _config.LoopGuard.TimeoutSeconds;
|
||||
set => _config.LoopGuard.TimeoutSeconds = Math.Max(10, value);
|
||||
}
|
||||
|
||||
// ──────────────── Kontext-Management ────────────────
|
||||
|
||||
[Category("3 - Kontext-Management")]
|
||||
[DisplayName("Max. Kontext-Tokens")]
|
||||
[Description("Maximales Token-Budget für den gesamten Konversationskontext. Bei Überschreitung wird automatisch kompaktiert.")]
|
||||
public int MaxContextTokens
|
||||
{
|
||||
get => _config.LoopGuard.MaxContextTokens;
|
||||
set => _config.LoopGuard.MaxContextTokens = Math.Max(10_000, value);
|
||||
}
|
||||
|
||||
[Category("3 - Kontext-Management")]
|
||||
[DisplayName("Kompaktierungs-Schwelle (%)")]
|
||||
[Description("Ab welchem Prozentsatz der Max. Kontext-Tokens wird kompaktiert. 80 = bei 80% Auslastung. Stufe 1: Tool-Results kürzen. Stufe 2: LLM-Zusammenfassung.")]
|
||||
public int CompactionThresholdPercent
|
||||
{
|
||||
get => (int)(_config.LoopGuard.CompactionThreshold * 100);
|
||||
set => _config.LoopGuard.CompactionThreshold = Math.Clamp(value, 50, 95) / 100.0;
|
||||
}
|
||||
|
||||
[Category("3 - Kontext-Management")]
|
||||
[DisplayName("Modell für Zusammenfassungen")]
|
||||
[Description("Modell, mit dem beim Kompaktieren zusammengefasst wird. Leer = Modell des Agenten. " +
|
||||
"Zusammenfassen ist anspruchslos — ein günstiges Modell spart hier deutlich, " +
|
||||
"da bis zu 30.000 Zeichen verarbeitet werden.")]
|
||||
public string SummaryModel
|
||||
{
|
||||
get => _config.LoopGuard.SummaryModel;
|
||||
set => _config.LoopGuard.SummaryModel = value ?? "";
|
||||
}
|
||||
|
||||
[Category("3 - Kontext-Management")]
|
||||
[DisplayName("Max. Zeichen pro Tool-Ergebnis")]
|
||||
[Description("Längere Tool-Ergebnisse werden gekürzt, bevor sie in den Kontext gelangen. " +
|
||||
"Ohne Grenze kann ein einzelner Abruf den Kontext sprengen — 512 KB entsprechen " +
|
||||
"etwa 130.000 Tokens in einer einzigen Antwort.")]
|
||||
public int MaxToolResultChars
|
||||
{
|
||||
get => _config.MaxToolResultChars;
|
||||
set => _config.MaxToolResultChars = Math.Max(1_000, value);
|
||||
}
|
||||
|
||||
// ──────────────── Kosten ────────────────
|
||||
|
||||
[Category("5 - Kosten")]
|
||||
[DisplayName("Prompt-Caching")]
|
||||
[Description("auto = für Modelle aktivieren, die es unterstützen; on = erzwingen; off = aus. " +
|
||||
"Spart erheblich, weil jeder Schritt eines Runs den kompletten Prompt erneut sendet — " +
|
||||
"System-Prompt und Tool-Definitionen also dutzendfach.")]
|
||||
[TypeConverter(typeof(PromptCachingConverter))]
|
||||
public string PromptCaching
|
||||
{
|
||||
get => _config.PromptCaching;
|
||||
set => _config.PromptCaching = string.IsNullOrWhiteSpace(value) ? "auto" : value;
|
||||
}
|
||||
|
||||
[Category("5 - Kosten")]
|
||||
[DisplayName("Tagesbudget (USD)")]
|
||||
[Description("Hartes Tageslimit für diesen Agenten in US-Dollar. 0 = kein Limit. " +
|
||||
"Bei Überschreitung wird kein Lauf mehr gestartet. " +
|
||||
"Greift nur, wenn für das Modell Preise vorliegen — sonst zusätzlich " +
|
||||
"das Token-Tageslimit setzen.")]
|
||||
public decimal DailyBudgetUsd
|
||||
{
|
||||
get => _config.Budget.DailyCostUsd;
|
||||
set => _config.Budget.DailyCostUsd = Math.Max(0, value);
|
||||
}
|
||||
|
||||
[Category("5 - Kosten")]
|
||||
[DisplayName("Token-Tageslimit")]
|
||||
[Description("Hartes Tageslimit für diesen Agenten in Tokens. 0 = kein Limit. " +
|
||||
"Wirkt auch dann, wenn für das Modell keine Preise vorliegen.")]
|
||||
public long DailyTokenLimit
|
||||
{
|
||||
get => _config.Budget.DailyTokens;
|
||||
set => _config.Budget.DailyTokens = Math.Max(0, value);
|
||||
}
|
||||
|
||||
// ──────────────── Tools (Read-Only) ────────────────
|
||||
|
||||
[Category("4 - Tools")]
|
||||
[DisplayName("Zugewiesene Tools")]
|
||||
[Description("Liste der Tool-Namen, die diesem Agent zugewiesen sind. Zuweisung über die Tabelle unten.")]
|
||||
[ReadOnly(true)]
|
||||
public string AssignedTools =>
|
||||
_config.Tools.Count == 0
|
||||
? "(keine)"
|
||||
: string.Join(", ", _config.Tools.Keys);
|
||||
|
||||
[Category("4 - Tools")]
|
||||
[DisplayName("Anzahl")]
|
||||
[ReadOnly(true)]
|
||||
public int ToolCount => _config.Tools.Count;
|
||||
|
||||
// ──────────────── Intern ────────────────
|
||||
|
||||
[Browsable(false)]
|
||||
public AgentConfig UnderlyingConfig => _config;
|
||||
|
||||
public override string ToString() =>
|
||||
string.IsNullOrWhiteSpace(_config.DisplayName) ? _config.AgentId : _config.DisplayName;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace ClawdDotNet.Models;
|
||||
|
||||
public sealed class AgentToolDisplayEntry
|
||||
{
|
||||
public bool Assigned { get; set; }
|
||||
public string ToolName { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using ClawdDotNet.Core.Config;
|
||||
|
||||
namespace ClawdDotNet.Models;
|
||||
|
||||
/// <summary>
|
||||
/// PropertyGrid-freundlicher Wrapper um InstanceConfig.
|
||||
/// Änderungen werden direkt im zugrunde liegenden InstanceConfig-Objekt gespeichert.
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
public sealed class InstanceSettingsViewModel
|
||||
{
|
||||
private readonly InstanceConfig _config;
|
||||
|
||||
public InstanceSettingsViewModel(InstanceConfig config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[Category("Instanz")]
|
||||
[DisplayName("Instanz-ID")]
|
||||
[Description("Eindeutige ID dieser laufenden Instanz.")]
|
||||
public string InstanceId
|
||||
{
|
||||
get => _config.InstanceId;
|
||||
set => _config.InstanceId = value;
|
||||
}
|
||||
|
||||
[Category("Instanz")]
|
||||
[DisplayName("Instanzname")]
|
||||
[Description("Anzeigename dieser Instanz (z.B. 'Aktien-Team').")]
|
||||
public string InstanceName
|
||||
{
|
||||
get => _config.InstanceName;
|
||||
set => _config.InstanceName = value;
|
||||
}
|
||||
|
||||
[Category("API")]
|
||||
[DisplayName("OpenRouter API-Key")]
|
||||
[Description("API-Schlüssel für OpenRouter. Wird für alle Agenten dieser Instanz verwendet.")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string OpenRouterApiKey
|
||||
{
|
||||
get => _config.OpenRouterApiKey;
|
||||
set => _config.OpenRouterApiKey = value;
|
||||
}
|
||||
|
||||
[Category("Verzeichnisse")]
|
||||
[DisplayName("Arbeitsverzeichnis")]
|
||||
[Description("Basis-Arbeitsverzeichnis für diese Instanz.")]
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get => _config.WorkingDirectory;
|
||||
set => _config.WorkingDirectory = value;
|
||||
}
|
||||
|
||||
[Category("Verzeichnisse")]
|
||||
[DisplayName("Log-Verzeichnis")]
|
||||
[Description("Verzeichnis für Log-Dateien dieser Instanz.")]
|
||||
public string LogDirectory
|
||||
{
|
||||
get => _config.LogDirectory;
|
||||
set => _config.LogDirectory = value;
|
||||
}
|
||||
|
||||
[Category("Netzwerk")]
|
||||
[DisplayName("Webserver-Port")]
|
||||
[Description("Port für den integrierten Webserver (0 = deaktiviert).")]
|
||||
public int WebServerPort
|
||||
{
|
||||
get => _config.WebServerPort;
|
||||
set => _config.WebServerPort = value;
|
||||
}
|
||||
|
||||
[Category("Agenten")]
|
||||
[DisplayName("Anzahl Agenten")]
|
||||
[Description("Anzahl der konfigurierten Agenten in dieser Instanz.")]
|
||||
[ReadOnly(true)]
|
||||
public int AgentCount => _config.Agents.Count;
|
||||
|
||||
// ─── WatchDog (Instanz-Heartbeat) ───
|
||||
// Server-URL und Master-Token stehen app-weit in den Anwendungseinstellungen. Die
|
||||
// Instanz registriert sich damit selbst; hier stehen nur Source/Gruppe/Intervall.
|
||||
// Ein/Aus läuft über den Service „Instanz-Watchdog" im Worker-Tab. Änderungen greifen
|
||||
// beim nächsten Start der Instanz.
|
||||
|
||||
[Category("WatchDog")]
|
||||
[DisplayName("Source")]
|
||||
[Description("Dienst-Kennung im WatchDog. Alle Instanzen teilen sich dieselbe Source.")]
|
||||
public string WatchdogSource
|
||||
{
|
||||
get => _config.Watchdog.Source;
|
||||
set => _config.Watchdog.Source = value;
|
||||
}
|
||||
|
||||
[Category("WatchDog")]
|
||||
[DisplayName("Gruppe")]
|
||||
[Description("Gruppierung im WatchDog-Dashboard.")]
|
||||
public string WatchdogGroup
|
||||
{
|
||||
get => _config.Watchdog.Group;
|
||||
set => _config.Watchdog.Group = value;
|
||||
}
|
||||
|
||||
[Category("WatchDog")]
|
||||
[DisplayName("Intervall (Sek)")]
|
||||
[Description("Sende-Takt der Heartbeats in Sekunden (mindestens 5).")]
|
||||
public int WatchdogIntervalSeconds
|
||||
{
|
||||
get => _config.Watchdog.IntervalSeconds;
|
||||
set => _config.Watchdog.IntervalSeconds = value;
|
||||
}
|
||||
|
||||
[Category("WatchDog")]
|
||||
[DisplayName("Token registriert")]
|
||||
[Description("Ob diese Instanz bereits einen eigenen Agent-Token vom WatchDog erhalten hat.")]
|
||||
[ReadOnly(true)]
|
||||
public bool WatchdogTokenRegistered => _config.Watchdog.HasToken;
|
||||
|
||||
[Browsable(false)]
|
||||
public InstanceConfig UnderlyingConfig => _config;
|
||||
|
||||
public override string ToString() => _config.InstanceName;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace ClawdDotNet.Models;
|
||||
|
||||
public sealed class JobDisplayEntry
|
||||
{
|
||||
public string JobType { get; set; } = "Agent Wakeup";
|
||||
public string AgentId { get; set; } = "";
|
||||
public string AgentName { get; set; } = "";
|
||||
public string ToolName { get; set; } = "";
|
||||
public string CronExpression { get; set; } = "";
|
||||
public string TaskMessage { get; set; } = "";
|
||||
public string NextRun { get; set; } = "—";
|
||||
public string LastRun { get; set; } = "—";
|
||||
public string LastStatus { get; set; } = "—";
|
||||
public bool RunOnStart { get; set; }
|
||||
public string Status { get; set; } = "Aktiv";
|
||||
public string JobId { get; set; } = "";
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using ClawdDotNet.Core.Api;
|
||||
using ClawdDotNet.Core.Api.Models;
|
||||
|
||||
namespace ClawdDotNet.Models;
|
||||
|
||||
/// <summary>
|
||||
/// TypeConverter der im PropertyGrid eine Dropdown-Liste
|
||||
/// mit verfügbaren OpenRouter-Modellen anzeigt.
|
||||
/// Die Modelle werden einmalig per API abgerufen und gecached.
|
||||
/// Freitext-Eingabe bleibt weiterhin möglich (CanConvertFrom = true).
|
||||
/// </summary>
|
||||
public sealed class ModelTypeConverter : StringConverter
|
||||
{
|
||||
private static List<ModelInfo>? _cachedModels;
|
||||
private static bool _fetchInProgress;
|
||||
private static readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Wird von außen gesetzt (beim Start der Anwendung), damit
|
||||
/// der Converter Zugriff auf den OpenRouterClient hat.
|
||||
/// </summary>
|
||||
public static OpenRouterClient? Client { get; set; }
|
||||
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
/// <summary>false = Dropdown ist editierbar (Freitext erlaubt)</summary>
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => false;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
EnsureModelsLoaded();
|
||||
|
||||
if (_cachedModels is null || _cachedModels.Count == 0)
|
||||
{
|
||||
// Fallback: Einige gängige Modelle
|
||||
return new StandardValuesCollection(new[]
|
||||
{
|
||||
"anthropic/claude-sonnet-4-5",
|
||||
"anthropic/claude-haiku-4",
|
||||
"openai/gpt-4.1",
|
||||
"openai/gpt-4.1-mini",
|
||||
"google/gemini-2.5-pro-preview",
|
||||
"google/gemini-2.5-flash-preview",
|
||||
"deepseek/deepseek-chat-v3-0324",
|
||||
"meta-llama/llama-4-maverick"
|
||||
});
|
||||
}
|
||||
|
||||
var ids = _cachedModels.Select(m => m.Id).ToArray();
|
||||
return new StandardValuesCollection(ids);
|
||||
}
|
||||
|
||||
private static void EnsureModelsLoaded()
|
||||
{
|
||||
if (_cachedModels is not null || Client is null)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_cachedModels is not null || _fetchInProgress)
|
||||
return;
|
||||
|
||||
_fetchInProgress = true;
|
||||
}
|
||||
|
||||
// Asynchronen Abruf im Hintergrund starten
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var models = await Client.GetAvailableModelsAsync();
|
||||
_cachedModels = models;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_cachedModels = []; // Fehler → Fallback wird verwendet
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lock) { _fetchInProgress = false; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kann von außen aufgerufen werden um den Cache zu leeren
|
||||
/// (z.B. wenn sich der API-Key ändert).
|
||||
/// </summary>
|
||||
public static void InvalidateCache()
|
||||
{
|
||||
lock (_lock) { _cachedModels = null; }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace ClawdDotNet.Models;
|
||||
|
||||
public sealed class ServiceDisplayEntry
|
||||
{
|
||||
public string ServiceId { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public int Port { get; set; }
|
||||
public string Status { get; set; } = "Gestoppt";
|
||||
public string StartedAt { get; set; } = "—";
|
||||
public string Description { get; set; } = "";
|
||||
public bool BuiltIn { get; set; }
|
||||
}
|
||||
@@ -1,566 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ClawdDotNet.Models;
|
||||
|
||||
static class ConfigHelper
|
||||
{
|
||||
public static string GetString(Dictionary<string, object?> config, string key, string fallback = "")
|
||||
{
|
||||
var val = config.GetValueOrDefault(key);
|
||||
return val switch
|
||||
{
|
||||
JsonElement je when je.ValueKind == JsonValueKind.String => je.GetString() ?? fallback,
|
||||
JsonElement je => je.ToString(),
|
||||
string s => s,
|
||||
null => fallback,
|
||||
_ => val.ToString() ?? fallback
|
||||
};
|
||||
}
|
||||
|
||||
public static int GetInt(Dictionary<string, object?> config, string key, int fallback = 0)
|
||||
{
|
||||
var val = config.GetValueOrDefault(key);
|
||||
return val switch
|
||||
{
|
||||
JsonElement je when je.ValueKind == JsonValueKind.Number => je.GetInt32(),
|
||||
JsonElement je => int.TryParse(je.ToString(), out var r) ? r : fallback,
|
||||
int i => i,
|
||||
_ => int.TryParse(val?.ToString(), out var r) ? r : fallback
|
||||
};
|
||||
}
|
||||
|
||||
public static bool GetBool(Dictionary<string, object?> config, string key, bool fallback = false)
|
||||
{
|
||||
var val = config.GetValueOrDefault(key);
|
||||
return val switch
|
||||
{
|
||||
JsonElement je when je.ValueKind is JsonValueKind.True => true,
|
||||
JsonElement je when je.ValueKind is JsonValueKind.False => false,
|
||||
JsonElement je => bool.TryParse(je.ToString(), out var r) ? r : fallback,
|
||||
bool b => b,
|
||||
_ => bool.TryParse(val?.ToString(), out var r) ? r : fallback
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetStringArray(Dictionary<string, object?> config, string key)
|
||||
{
|
||||
var val = config.GetValueOrDefault(key);
|
||||
return val switch
|
||||
{
|
||||
JsonElement je when je.ValueKind == JsonValueKind.Array =>
|
||||
string.Join(",", je.EnumerateArray().Select(e => e.GetString())),
|
||||
object[] arr => string.Join(",", arr),
|
||||
string s => s,
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum FileRWAccessLevel
|
||||
{
|
||||
Denied,
|
||||
Read,
|
||||
ReadWrite,
|
||||
Admin
|
||||
}
|
||||
|
||||
public sealed class FileRWToolSettings
|
||||
{
|
||||
[Category("Persönlicher Workspace")]
|
||||
[DisplayName("Erlaubte Endungen")]
|
||||
[Description("Dateiendungen für den eigenen Agenten-Workspace (z.B. .txt,.json,.md)")]
|
||||
public string PersonalAllowedExtensions { get; set; } = ".txt,.json,.md,.html,.js,.css";
|
||||
|
||||
[Category("Shared Workspace")]
|
||||
[DisplayName("Zugriffslevel")]
|
||||
[Description("Legt fest, welche Operationen im SharedWorkspace erlaubt sind")]
|
||||
public FileRWAccessLevel SharedAccessLevel { get; set; } = FileRWAccessLevel.Denied;
|
||||
|
||||
[Category("Shared Workspace")]
|
||||
[DisplayName("Erlaubte Endungen")]
|
||||
[Description("Dateiendungen für den geteilten Workspace")]
|
||||
public string SharedAllowedExtensions { get; set; } = ".txt,.json,.md";
|
||||
|
||||
[Category("Shared Workspace – Schutz")]
|
||||
[DisplayName("Geschützte Pfade")]
|
||||
[Description("Komma-getrennte Pfade im SharedWorkspace die append-only sind (z.B. stocks/,archives/). Dateien dort können nur erstellt, nicht überschrieben oder gelöscht werden. Admin-Level umgeht den Schutz.")]
|
||||
public string ProtectedPaths { get; set; } = "stocks/";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["personalAllowedExtensions"] = PersonalAllowedExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
|
||||
["sharedAccessLevel"] = SharedAccessLevel.ToString(),
|
||||
["sharedAllowedExtensions"] = SharedAllowedExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
|
||||
["protectedPaths"] = ProtectedPaths.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
};
|
||||
|
||||
public static FileRWToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
PersonalAllowedExtensions = ConfigHelper.GetStringArray(config, "personalAllowedExtensions") is { Length: > 0 } s1
|
||||
? s1 : (ConfigHelper.GetStringArray(config, "allowedExtensions") is { Length: > 0 } sOld ? sOld : ".txt,.json,.md,.html,.js,.css"),
|
||||
|
||||
SharedAccessLevel = Enum.TryParse<FileRWAccessLevel>(ConfigHelper.GetString(config, "sharedAccessLevel"), true, out var level)
|
||||
? level : FileRWAccessLevel.Denied,
|
||||
|
||||
SharedAllowedExtensions = ConfigHelper.GetStringArray(config, "sharedAllowedExtensions") is { Length: > 0 } s2
|
||||
? s2 : ".txt,.json,.md",
|
||||
|
||||
ProtectedPaths = ConfigHelper.GetStringArray(config, "protectedPaths") is { Length: > 0 } s3
|
||||
? s3 : "stocks/"
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class MailToolSettings
|
||||
{
|
||||
[Category("Mail - Konto")]
|
||||
[DisplayName("Benutzername")]
|
||||
public string Username { get; set; } = "";
|
||||
|
||||
[Category("Mail - Konto")]
|
||||
[DisplayName("Passwort")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[Category("Mail - IMAP")]
|
||||
[DisplayName("IMAP-Host")]
|
||||
public string ImapHost { get; set; } = "";
|
||||
|
||||
[Category("Mail - IMAP")]
|
||||
[DisplayName("IMAP-Port")]
|
||||
public int ImapPort { get; set; } = 993;
|
||||
|
||||
[Category("Mail - SMTP")]
|
||||
[DisplayName("SMTP-Host")]
|
||||
public string SmtpHost { get; set; } = "";
|
||||
|
||||
[Category("Mail - SMTP")]
|
||||
[DisplayName("SMTP-Port")]
|
||||
public int SmtpPort { get; set; } = 587;
|
||||
|
||||
[Category("Mail - Sicherheit")]
|
||||
[DisplayName("Erlaubte Empfänger")]
|
||||
[Description("Komma-getrennte Liste erlaubter E-Mail-Adressen")]
|
||||
public string AllowedRecipients { get; set; } = "";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["username"] = Username,
|
||||
["password"] = Password,
|
||||
["imapHost"] = ImapHost,
|
||||
["imapPort"] = ImapPort,
|
||||
["smtpHost"] = SmtpHost,
|
||||
["smtpPort"] = SmtpPort,
|
||||
["allowedRecipients"] = AllowedRecipients.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
};
|
||||
|
||||
public static MailToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
Username = ConfigHelper.GetString(config, "username"),
|
||||
Password = ConfigHelper.GetString(config, "password"),
|
||||
ImapHost = ConfigHelper.GetString(config, "imapHost"),
|
||||
ImapPort = ConfigHelper.GetInt(config, "imapPort", 993),
|
||||
SmtpHost = ConfigHelper.GetString(config, "smtpHost"),
|
||||
SmtpPort = ConfigHelper.GetInt(config, "smtpPort", 587),
|
||||
AllowedRecipients = ConfigHelper.GetStringArray(config, "allowedRecipients")
|
||||
};
|
||||
}
|
||||
|
||||
public enum DatabaseType
|
||||
{
|
||||
MySql,
|
||||
Postgres,
|
||||
MsSql,
|
||||
MongoDb
|
||||
}
|
||||
|
||||
public enum DatabaseAccessLevel
|
||||
{
|
||||
[Description("Nur Lesen (SELECT/find)")]
|
||||
ReadOnly,
|
||||
[Description("Lesen und Schreiben (INSERT/UPDATE/DELETE)")]
|
||||
ReadWrite,
|
||||
[Description("Vollzugriff (Admin/Schema-Änderungen)")]
|
||||
Admin
|
||||
}
|
||||
|
||||
public sealed class DatabaseToolSettings
|
||||
{
|
||||
[Category("Datenbank")]
|
||||
[DisplayName("Typ")]
|
||||
[Description("Der zu verwendende Datenbanktyp")]
|
||||
public DatabaseType Type { get; set; } = DatabaseType.MySql;
|
||||
|
||||
[Category("Datenbank")]
|
||||
[DisplayName("Connection-String")]
|
||||
public string ConnectionString { get; set; } = "";
|
||||
|
||||
[Category("Datenbank")]
|
||||
[DisplayName("Zugriffsebene")]
|
||||
[Description("Legt fest, welche Operationen der Agent ausführen darf")]
|
||||
public DatabaseAccessLevel AccessLevel { get; set; } = DatabaseAccessLevel.ReadOnly;
|
||||
|
||||
[Category("Datenbank - Sicherheit")]
|
||||
[DisplayName("Erlaubte Tabellen")]
|
||||
[Description("Komma-getrennte Liste erlaubter Tabellen/Collections")]
|
||||
public string AllowedTables { get; set; } = "";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["type"] = Type.ToString().ToLowerInvariant(),
|
||||
["connectionString"] = ConnectionString,
|
||||
["accessLevel"] = AccessLevel.ToString(),
|
||||
["allowedTables"] = AllowedTables.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
};
|
||||
|
||||
public static DatabaseToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
Type = Enum.TryParse<DatabaseType>(config.GetValueOrDefault("type")?.ToString(), true, out var result) ? result : DatabaseType.MySql,
|
||||
ConnectionString = config.GetValueOrDefault("connectionString")?.ToString() ?? "",
|
||||
AccessLevel = Enum.TryParse<DatabaseAccessLevel>(config.GetValueOrDefault("accessLevel")?.ToString() ?? config.GetValueOrDefault("allowWrite")?.ToString(), true, out var level)
|
||||
? level
|
||||
: (config.GetValueOrDefault("allowWrite") is true or "True" or "true" ? DatabaseAccessLevel.ReadWrite : DatabaseAccessLevel.ReadOnly),
|
||||
AllowedTables = config.GetValueOrDefault("allowedTables") is object[] arr
|
||||
? string.Join(",", arr)
|
||||
: config.GetValueOrDefault("allowedTables")?.ToString() ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class FTPToolSettings
|
||||
{
|
||||
[Category("FTP Server")]
|
||||
[DisplayName("Host")]
|
||||
public string Host { get; set; } = "";
|
||||
|
||||
[Category("FTP Server")]
|
||||
[DisplayName("Port")]
|
||||
public int Port { get; set; } = 21;
|
||||
|
||||
[Category("FTP Server")]
|
||||
[DisplayName("Benutzername")]
|
||||
public string Username { get; set; } = "";
|
||||
|
||||
[Category("FTP Server")]
|
||||
[DisplayName("Passwort")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[Category("FTP Lokal")]
|
||||
[DisplayName("Root-Pfad")]
|
||||
[Description("Basisverzeichnis für Dateiübertragungen")]
|
||||
public string RootPath { get; set; } = "./data/";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["host"] = Host,
|
||||
["port"] = Port,
|
||||
["username"] = Username,
|
||||
["password"] = Password,
|
||||
["rootPath"] = RootPath
|
||||
};
|
||||
|
||||
public static FTPToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
Host = ConfigHelper.GetString(config, "host"),
|
||||
Port = ConfigHelper.GetInt(config, "port", 21),
|
||||
Username = ConfigHelper.GetString(config, "username"),
|
||||
Password = ConfigHelper.GetString(config, "password"),
|
||||
RootPath = ConfigHelper.GetString(config, "rootPath", "./data/")
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class TelegramToolSettings
|
||||
{
|
||||
[Category("Telegram")]
|
||||
[DisplayName("Bot-Token")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string BotToken { get; set; } = "";
|
||||
|
||||
[Category("Telegram")]
|
||||
[DisplayName("Standard Chat-ID")]
|
||||
[Description("Die Standard-ID, an die Nachrichten gesendet werden, wenn keine andere ID angegeben ist.")]
|
||||
public string DefaultChatId { get; set; } = "";
|
||||
|
||||
[Category("Telegram - Sicherheit")]
|
||||
[DisplayName("Erlaubte Chat-IDs")]
|
||||
[Description("Komma-getrennte Liste erlaubter Chat-IDs")]
|
||||
public string AllowedChatIds { get; set; } = "";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["botToken"] = BotToken,
|
||||
["defaultChatId"] = DefaultChatId,
|
||||
["allowedChatIds"] = AllowedChatIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
};
|
||||
|
||||
public static TelegramToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
BotToken = ConfigHelper.GetString(config, "botToken"),
|
||||
DefaultChatId = ConfigHelper.GetString(config, "defaultChatId"),
|
||||
AllowedChatIds = ConfigHelper.GetStringArray(config, "allowedChatIds")
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class DirectAPIToolSettings
|
||||
{
|
||||
[Category("DirectAPI")]
|
||||
[DisplayName("Standard-Provider")]
|
||||
public string DefaultProvider { get; set; } = "twelvedata";
|
||||
|
||||
[Category("DirectAPI")]
|
||||
[DisplayName("Cache TTL (Sekunden)")]
|
||||
public int CacheTtlSeconds { get; set; } = 60;
|
||||
|
||||
[Category("DirectAPI - API Keys")]
|
||||
[DisplayName("Twelve Data Key")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string TwelveDataKey { get; set; } = "";
|
||||
|
||||
[Category("DirectAPI - API Keys")]
|
||||
[DisplayName("Alpha Vantage Key")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string AlphaVantageKey { get; set; } = "";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["defaultProvider"] = DefaultProvider,
|
||||
["cacheTtlSeconds"] = CacheTtlSeconds,
|
||||
["providers"] = new Dictionary<string, object?>
|
||||
{
|
||||
["twelvedata"] = new { apiKey = TwelveDataKey },
|
||||
["alphavantage"] = new { apiKey = AlphaVantageKey }
|
||||
}
|
||||
};
|
||||
|
||||
public static DirectAPIToolSettings FromConfig(Dictionary<string, object?> config)
|
||||
{
|
||||
var settings = new DirectAPIToolSettings
|
||||
{
|
||||
DefaultProvider = ConfigHelper.GetString(config, "defaultProvider", "twelvedata"),
|
||||
CacheTtlSeconds = ConfigHelper.GetInt(config, "cacheTtlSeconds", 60)
|
||||
};
|
||||
|
||||
if (config.GetValueOrDefault("providers") is JsonElement providersJe)
|
||||
{
|
||||
var providers = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(providersJe.GetRawText());
|
||||
if (providers != null)
|
||||
{
|
||||
if (providers.TryGetValue("twelvedata", out var td) && td.TryGetProperty("apiKey", out var tdk))
|
||||
settings.TwelveDataKey = tdk.GetString() ?? "";
|
||||
if (providers.TryGetValue("alphavantage", out var av) && av.TryGetProperty("apiKey", out var avk))
|
||||
settings.AlphaVantageKey = avk.GetString() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WebFetchToolSettings
|
||||
{
|
||||
[Category("WebFetch")]
|
||||
[DisplayName("Erlaubte Domains")]
|
||||
[Description("Komma-getrennte Liste (z.B. reuters.com,bloomberg.com)")]
|
||||
public string AllowedDomains { get; set; } = "";
|
||||
|
||||
[Category("WebFetch")]
|
||||
[DisplayName("Max Response KB")]
|
||||
public int MaxResponseKb { get; set; } = 512;
|
||||
|
||||
[Category("WebFetch")]
|
||||
[DisplayName("User Agent")]
|
||||
public string UserAgent { get; set; } = "ClawdDotNet-Agent/1.0";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["allowedDomains"] = AllowedDomains.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
|
||||
["maxResponseKb"] = MaxResponseKb,
|
||||
["userAgent"] = UserAgent
|
||||
};
|
||||
|
||||
public static WebFetchToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
AllowedDomains = ConfigHelper.GetStringArray(config, "allowedDomains"),
|
||||
MaxResponseKb = ConfigHelper.GetInt(config, "maxResponseKb", 512),
|
||||
UserAgent = ConfigHelper.GetString(config, "userAgent", "ClawdDotNet-Agent/1.0")
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class WebMonitorToolSettings
|
||||
{
|
||||
[Category("WebMonitor")]
|
||||
[DisplayName("Monitore (JSON)")]
|
||||
[Description("JSON-Konfiguration der Monitore")]
|
||||
public string MonitorsJson { get; set; } = "{}";
|
||||
|
||||
public Dictionary<string, object?> ToConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["monitors"] = JsonSerializer.Deserialize<Dictionary<string, object?>>(MonitorsJson) ?? new()
|
||||
};
|
||||
}
|
||||
catch { return new Dictionary<string, object?> { ["monitors"] = new Dictionary<string, object?>() }; }
|
||||
}
|
||||
|
||||
public static WebMonitorToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
MonitorsJson = config.GetValueOrDefault("monitors") is JsonElement je ? je.GetRawText() : "{}"
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class AgentCommToolSettings
|
||||
{
|
||||
[Category("AgentComm")]
|
||||
[DisplayName("Info")]
|
||||
[Description("Dieses Tool benötigt keine Konfiguration. Es ermöglicht Agenten, mit anderen Agenten in der gleichen Instanz zu kommunizieren.")]
|
||||
[ReadOnly(true)]
|
||||
public string Status { get; set; } = "Aktiv";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new();
|
||||
|
||||
public static AgentCommToolSettings FromConfig(Dictionary<string, object?> config) => new();
|
||||
}
|
||||
|
||||
public sealed class SocialMediaManagerToolSettings
|
||||
{
|
||||
// ─── X (Twitter) ───
|
||||
|
||||
[Category("1. X (Twitter) - API")]
|
||||
[DisplayName("Bearer Token")]
|
||||
[Description("X API v2 Bearer Token für die Authentifizierung")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string XApiKey { get; set; } = "";
|
||||
|
||||
[Category("1. X (Twitter) - Monitoring")]
|
||||
[DisplayName("Überwachte Accounts")]
|
||||
[Description("Komma-getrennte Liste von X-Accounts die überwacht werden sollen (ohne @). Beispiel: elonmusk,unusual_whales,DeItaone")]
|
||||
public string XWatchAccounts { get; set; } = "";
|
||||
|
||||
// ─── Reddit ───
|
||||
|
||||
[Category("2. Reddit - Monitoring")]
|
||||
[DisplayName("Überwachte Subreddits")]
|
||||
[Description("Komma-getrennte Liste von Subreddits die überwacht werden sollen (ohne r/). Beispiel: wallstreetbets,stocks,options")]
|
||||
public string RedditWatchSubreddits { get; set; } = "";
|
||||
|
||||
[Category("2. Reddit - Monitoring")]
|
||||
[DisplayName("Posts pro Subreddit")]
|
||||
[Description("Maximale Anzahl Posts die pro Subreddit bei jedem Check abgerufen werden (Standard: 15)")]
|
||||
public int RedditPostLimit { get; set; } = 15;
|
||||
|
||||
// ─── YouTube / STT ───
|
||||
|
||||
[Category("3. YouTube / STT")]
|
||||
[DisplayName("OpenRouter API Key")]
|
||||
[Description("API Key für Speech-to-Text Transkription über OpenRouter")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string OpenRouterApiKey { get; set; } = "";
|
||||
|
||||
[Category("3. YouTube / STT")]
|
||||
[DisplayName("STT Modell")]
|
||||
[Description("OpenRouter Modell-ID für die Transkription")]
|
||||
public string STTModel { get; set; } = "openai/whisper-1";
|
||||
|
||||
[Category("3. YouTube / STT")]
|
||||
[DisplayName("YouTube Kanäle")]
|
||||
[Description("Komma-getrennte Liste von YouTube Kanal-URLs für automatische Überwachung")]
|
||||
public string YoutubeChannels { get; set; } = "";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new()
|
||||
{
|
||||
["xApiKey"] = XApiKey,
|
||||
["xWatchAccounts"] = SplitToArray(XWatchAccounts),
|
||||
["redditWatchSubreddits"] = SplitToArray(RedditWatchSubreddits),
|
||||
["redditPostLimit"] = RedditPostLimit,
|
||||
["openRouterApiKey"] = OpenRouterApiKey,
|
||||
["sttModel"] = STTModel,
|
||||
["youtubeChannels"] = SplitToArray(YoutubeChannels)
|
||||
};
|
||||
|
||||
public static SocialMediaManagerToolSettings FromConfig(Dictionary<string, object?> config) => new()
|
||||
{
|
||||
XApiKey = ConfigHelper.GetString(config, "xApiKey"),
|
||||
XWatchAccounts = ConfigHelper.GetStringArray(config, "xWatchAccounts"),
|
||||
RedditWatchSubreddits = ConfigHelper.GetStringArray(config, "redditWatchSubreddits"),
|
||||
RedditPostLimit = ConfigHelper.GetInt(config, "redditPostLimit", 15),
|
||||
OpenRouterApiKey = ConfigHelper.GetString(config, "openRouterApiKey"),
|
||||
STTModel = ConfigHelper.GetString(config, "sttModel", "openai/whisper-1"),
|
||||
YoutubeChannels = ConfigHelper.GetStringArray(config, "youtubeChannels")
|
||||
};
|
||||
|
||||
private static string[] SplitToArray(string csv)
|
||||
=> csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
public sealed class AgentEditorToolSettings
|
||||
{
|
||||
[Category("AgentEditor")]
|
||||
[DisplayName("Info")]
|
||||
[Description("Erlaubt dem Agenten, Identity und Soul anderer Agenten zu lesen, zu bearbeiten und neue Agenten zu erstellen. Keine weitere Konfiguration nötig.")]
|
||||
[ReadOnly(true)]
|
||||
public string Status { get; set; } = "Aktiv";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new();
|
||||
|
||||
public static AgentEditorToolSettings FromConfig(Dictionary<string, object?> config) => new();
|
||||
}
|
||||
|
||||
public sealed class AgentSpawnToolSettings
|
||||
{
|
||||
[Category("AgentSpawn")]
|
||||
[DisplayName("Info")]
|
||||
[Description("Dieses Tool benötigt keine Konfiguration. Es ermöglicht Agenten, andere Agenten zu starten und ihnen Aufgaben zuzuweisen.")]
|
||||
[ReadOnly(true)]
|
||||
public string Status { get; set; } = "Aktiv";
|
||||
|
||||
public Dictionary<string, object?> ToConfig() => new();
|
||||
|
||||
public static AgentSpawnToolSettings FromConfig(Dictionary<string, object?> config) => new();
|
||||
}
|
||||
|
||||
public static class ToolSettingsFactory
|
||||
{
|
||||
public static object? CreateViewModel(string toolName, Dictionary<string, object?>? config)
|
||||
{
|
||||
config ??= new();
|
||||
return toolName switch
|
||||
{
|
||||
"FileRW" => FileRWToolSettings.FromConfig(config),
|
||||
"Mail" => MailToolSettings.FromConfig(config),
|
||||
"Database" => DatabaseToolSettings.FromConfig(config),
|
||||
"Telegram" => TelegramToolSettings.FromConfig(config),
|
||||
"FTP" => FTPToolSettings.FromConfig(config),
|
||||
"DirectAPI" => DirectAPIToolSettings.FromConfig(config),
|
||||
"WebFetch" => WebFetchToolSettings.FromConfig(config),
|
||||
"WebMonitor" => WebMonitorToolSettings.FromConfig(config),
|
||||
"AgentComm" => AgentCommToolSettings.FromConfig(config),
|
||||
"SocialMediaManager" => SocialMediaManagerToolSettings.FromConfig(config),
|
||||
"AgentSpawn" => AgentSpawnToolSettings.FromConfig(config),
|
||||
"AgentEditor" => AgentEditorToolSettings.FromConfig(config),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public static Dictionary<string, object?>? ToConfig(string toolName, object? viewModel)
|
||||
{
|
||||
return viewModel switch
|
||||
{
|
||||
FileRWToolSettings f => f.ToConfig(),
|
||||
MailToolSettings m => m.ToConfig(),
|
||||
DatabaseToolSettings d => d.ToConfig(),
|
||||
TelegramToolSettings t => t.ToConfig(),
|
||||
FTPToolSettings ftp => ftp.ToConfig(),
|
||||
DirectAPIToolSettings dapi => dapi.ToConfig(),
|
||||
WebFetchToolSettings wf => wf.ToConfig(),
|
||||
WebMonitorToolSettings wm => wm.ToConfig(),
|
||||
AgentCommToolSettings ac => ac.ToConfig(),
|
||||
SocialMediaManagerToolSettings smm => smm.ToConfig(),
|
||||
AgentSpawnToolSettings asp => asp.ToConfig(),
|
||||
AgentEditorToolSettings ae => ae.ToConfig(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
using ClawdDotNet.Core.Api;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using ClawdDotNet.Core.Logging;
|
||||
using ClawdDotNet.Core.Security;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using ClawdDotNet.Core.State;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using ClawdDotNet.Core.Memory;
|
||||
using ClawdDotNet.Core.Accounting;
|
||||
using ClawdDotNet.Services;
|
||||
using ClawdDotNet.Tools.FileRW;
|
||||
using ClawdDotNet.Tools.Telegram;
|
||||
using ClawdDotNet.Tools.Mail;
|
||||
using ClawdDotNet.Tools.Database;
|
||||
using ClawdDotNet.Tools.FTP;
|
||||
using ClawdDotNet.Tools.DirectAPI;
|
||||
using ClawdDotNet.Tools.WebFetch;
|
||||
using ClawdDotNet.Tools.WebMonitor;
|
||||
using ClawdDotNet.Tools.AgentComm;
|
||||
using ClawdDotNet.Tools.AgentSpawn;
|
||||
using ClawdDotNet.Tools.SocialMediaManager;
|
||||
using ClawdDotNet.Tools.AgentEditor;
|
||||
using ClawdDotNet.Tools.TelegramClient;
|
||||
using ClawdDotNet.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main(string[] args)
|
||||
{
|
||||
ApplicationConfiguration.Initialize();
|
||||
|
||||
// ─── 0. Embedded UI extrahieren ───
|
||||
EmbeddedUiManager.ExtractToTemp();
|
||||
|
||||
// ─── 1. App-Settings laden ───
|
||||
var settingsManager = new SettingsManager();
|
||||
settingsManager.Load();
|
||||
var appSettings = settingsManager.AppSettings;
|
||||
|
||||
// ─── 2. InstanceDirectoryManager erstellen ───
|
||||
var instancesDir = Path.GetFullPath(appSettings.InstancesDirectory);
|
||||
var dirManager = new InstanceDirectoryManager(instancesDir);
|
||||
|
||||
// ─── 3. Instanz-Verzeichnis auswählen ───
|
||||
string instancePath;
|
||||
|
||||
#if DEBUG
|
||||
// Im Debug-Modus: Dev-Instanz automatisch erstellen/starten
|
||||
const string devInstanceName = "Dev";
|
||||
instancePath = dirManager.GetInstancePath(devInstanceName);
|
||||
|
||||
if (!Directory.Exists(instancePath))
|
||||
{
|
||||
dirManager.CreateInstance(devInstanceName);
|
||||
}
|
||||
#else
|
||||
// Im Release-Modus: InstanceManager anzeigen
|
||||
var instanceManager = new frm_InstanceManager(dirManager, settingsManager);
|
||||
var dialogResult = instanceManager.ShowDialog();
|
||||
|
||||
if (dialogResult != DialogResult.OK || string.IsNullOrWhiteSpace(instanceManager.SelectedInstancePath))
|
||||
{
|
||||
return; // Benutzer hat abgebrochen
|
||||
}
|
||||
|
||||
instancePath = instanceManager.SelectedInstancePath;
|
||||
instanceManager.Dispose();
|
||||
#endif
|
||||
|
||||
// ─── 4. Instanz-Config aus Verzeichnisstruktur laden ───
|
||||
InstanceConfig instanceConfig;
|
||||
try
|
||||
{
|
||||
instanceConfig = dirManager.LoadInstanceConfig(instancePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Fehler beim Laden der Instanz:\n{instancePath}\n\n{ex.Message}",
|
||||
"ClawdDotNet – Fehler",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── 5. Logging-System initialisieren ───
|
||||
var logDir = Path.GetFullPath(
|
||||
!string.IsNullOrWhiteSpace(instanceConfig.LogDirectory)
|
||||
? instanceConfig.LogDirectory
|
||||
: appSettings.LogDirectory);
|
||||
|
||||
var minLevel = Enum.TryParse<ClawdDotNet.Core.Logging.LogLevel>(
|
||||
appSettings.MinimumLogLevel, true, out var ml)
|
||||
? ml
|
||||
: ClawdDotNet.Core.Logging.LogLevel.Info;
|
||||
|
||||
var loggerFactory = LoggingExtensions.CreateClawdLoggerFactory(logDir, minLevel);
|
||||
var coreLogger = loggerFactory.CreateLogger("ClawdDotNet.Core.Startup");
|
||||
|
||||
coreLogger.LogInformation("ClawdDotNet startet – Instanz: {Instance} ({Id})",
|
||||
instanceConfig.InstanceName, instanceConfig.InstanceId);
|
||||
coreLogger.LogInformation("Instanz-Verzeichnis: {Path}", instancePath);
|
||||
|
||||
// ─── 5b. Lizenzprüfung ───
|
||||
// Entfällt in dieser Fassung. Startprüfung, laufende Nachprüfung und die
|
||||
// Deploymentcenter-Anbindung (Watchdog, Updates, Fehler-Stream, Bugtracker)
|
||||
// liegen in ClawdDotNet.App (AppHost); Einstiegspunkt ist
|
||||
// src/ClawdDotNet.Desktop. Diese Datei ist nur noch Vorlage — siehe
|
||||
// ClawdDotNet.slnx.
|
||||
|
||||
// ─── 6. Core-Komponenten erzeugen ───
|
||||
var toolRegistry = new ToolRegistry();
|
||||
toolRegistry.Register(new FileRWTool());
|
||||
toolRegistry.Register(new TelegramTool());
|
||||
toolRegistry.Register(new MailTool());
|
||||
toolRegistry.Register(new DatabaseTool());
|
||||
toolRegistry.Register(new FTPTool());
|
||||
toolRegistry.Register(new DirectApiTool());
|
||||
toolRegistry.Register(new WebFetchTool());
|
||||
toolRegistry.Register(new WebMonitorTool());
|
||||
toolRegistry.Register(new AgentCommTool());
|
||||
toolRegistry.Register(new SocialMediaManagerTool());
|
||||
toolRegistry.Register(new AgentSpawnTool());
|
||||
toolRegistry.Register(new AgentEditorTool());
|
||||
toolRegistry.Register(new ClawdDotNet.Tools.Memory.MemoryTool());
|
||||
toolRegistry.Register(new ClawdDotNet.Tools.Taskboard.TaskboardTool());
|
||||
|
||||
// ─── 6a. TelegramClient (MTProto User-API) ───
|
||||
TelegramClientManager? tgClientManager = null;
|
||||
if (instanceConfig.TelegramClient is not null)
|
||||
{
|
||||
tgClientManager = new TelegramClientManager(
|
||||
instanceConfig,
|
||||
instancePath,
|
||||
loggerFactory.CreateLogger("ClawdDotNet.Tools.TelegramClient"));
|
||||
toolRegistry.Register(new TelegramClientTool(tgClientManager));
|
||||
coreLogger.LogInformation("TelegramClient-Tool registriert");
|
||||
}
|
||||
|
||||
var permissionGate = new PermissionGate();
|
||||
|
||||
// TODO: Tools aus tools/ Ordner laden
|
||||
// foreach (var toolDll in Directory.GetFiles("./tools", "*.dll"))
|
||||
// LoadToolPlugin(toolDll, toolRegistry);
|
||||
|
||||
OpenRouterClient? openRouterClient = null;
|
||||
AgentEngine? agentEngine = null;
|
||||
ClawdDotNet.Core.Tasks.TaskScanner? taskScanner = null;
|
||||
ClawdDotNet.Core.Staging.StagingService? stagingService = null;
|
||||
SqliteUsageRepository? usageRepository = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(instanceConfig.OpenRouterApiKey))
|
||||
{
|
||||
openRouterClient = new OpenRouterClient(
|
||||
instanceConfig.OpenRouterApiKey,
|
||||
loggerFactory.CreateLogger("ClawdDotNet.Core.Api.OpenRouterClient"));
|
||||
|
||||
// ModelTypeConverter mit dem Client verbinden für PropertyGrid-Dropdown
|
||||
ClawdDotNet.Models.ModelTypeConverter.Client = openRouterClient;
|
||||
|
||||
// ─── 6.1. Speicher initialisieren ───
|
||||
// Eine Datenbank je Instanz; StateStore und Gedächtnis teilen sie sich.
|
||||
var storage = new SqliteStorage(Path.Combine(instancePath, "state.db"));
|
||||
var stateStore = new SqliteStateStore(storage);
|
||||
var memoryRepository = new SqliteMemoryRepository(storage);
|
||||
var taskRepository = new ClawdDotNet.Core.Tasks.SqliteTaskRepository(storage);
|
||||
var auditRepository = new ClawdDotNet.Core.Audit.SqliteAuditRepository(storage);
|
||||
var stagingRepository = new ClawdDotNet.Core.Staging.SqliteStagingRepository(storage);
|
||||
var stagingGate = new ClawdDotNet.Core.Staging.StagingGate(
|
||||
new ClawdDotNet.Core.Staging.StagingPolicy(), stagingRepository);
|
||||
usageRepository = new SqliteUsageRepository(storage);
|
||||
|
||||
// Preise fürs Budget: Ohne sie greift nur die Token-Grenze.
|
||||
var pricingCatalog = new ModelPricingCatalog();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { pricingCatalog.Load(await openRouterClient.GetAvailableModelsAsync()); }
|
||||
catch { /* Ohne Preise bleibt die Kostengrenze wirkungslos, die Token-Grenze nicht. */ }
|
||||
});
|
||||
|
||||
agentEngine = new AgentEngine(
|
||||
openRouterClient, toolRegistry, permissionGate, stateStore, loggerFactory,
|
||||
memoryRepository, usageRepository, pricingCatalog, taskRepository, auditRepository, stagingGate)
|
||||
{
|
||||
InstanceBudget = instanceConfig.Budget
|
||||
};
|
||||
agentEngine.SetAgentConfigProvider(
|
||||
() => instanceConfig.Agents,
|
||||
instanceConfig.InstanceId,
|
||||
agentId =>
|
||||
{
|
||||
var agent = instanceConfig.Agents.FirstOrDefault(a => a.AgentId == agentId);
|
||||
if (agent is null) return null;
|
||||
return string.IsNullOrWhiteSpace(agent.AgentDir) ? null : agent.AgentDir;
|
||||
});
|
||||
agentEngine.LoadPersistedChats();
|
||||
|
||||
// ─── Taskboard: Dateien in die DB spiegeln (Reconciliation beim Start) ───
|
||||
// Verwaiste Claims eines abgestürzten Laufs lösen, dann alle Task-Dateien
|
||||
// importieren. Nicht blockierend — der Start soll nicht auf das Dateisystem warten.
|
||||
var sharedWorkspace = instanceConfig.Agents
|
||||
.Select(a => a.SharedWorkspacePath)
|
||||
.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p));
|
||||
if (!string.IsNullOrWhiteSpace(sharedWorkspace))
|
||||
{
|
||||
var board = new ClawdDotNet.Core.Tasks.TaskboardService(
|
||||
taskRepository, Path.Combine(sharedWorkspace, "tasks"));
|
||||
|
||||
// Staging (A2): Freigabe-/Ablehnungs-Dienst für die Review-Oberfläche.
|
||||
// Weckt den Agenten nach der Entscheidung über einen Folge-Task (A1).
|
||||
stagingService = new ClawdDotNet.Core.Staging.StagingService(
|
||||
stagingRepository, agentEngine, board, loggerFactory, auditRepository);
|
||||
|
||||
// Scanner (A1): der Taktgeber. Startet erst, wenn die Reconciliation durch
|
||||
// ist — sonst könnte er einen noch nicht bereinigten Claim antreffen.
|
||||
// Der Scanner ist der EINZIGE periodische Treiber (A1) — er ersetzt die
|
||||
// früheren AgentScheduler/ToolJobScheduler. Geplante Agent-Läufe wie auch
|
||||
// Tool-Job-Polls sind jetzt Tasks.
|
||||
var dispatcher = new ClawdDotNet.Core.Tasks.EngineTaskDispatcher(
|
||||
agentEngine, () => instanceConfig.Agents, instanceConfig.InstanceId,
|
||||
toolRegistry, stateStore, loggerFactory);
|
||||
taskScanner = new ClawdDotNet.Core.Tasks.TaskScanner(
|
||||
taskRepository, dispatcher, loggerFactory);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var reset = await taskRepository.ReleaseStaleClaimsAsync(
|
||||
DateTime.UtcNow.AddMinutes(-15), DateTime.UtcNow, CancellationToken.None);
|
||||
|
||||
// Einmalige Übernahme der alten coordination/task_*-Dateien …
|
||||
var coordMigration = new ClawdDotNet.Core.Tasks.CoordinationMigration(
|
||||
board, Path.Combine(sharedWorkspace, "coordination"), loggerFactory);
|
||||
var migratedCoord = await coordMigration.RunAsync(CancellationToken.None);
|
||||
|
||||
// … und der alten scheduler/toolJobs-Konfiguration in Tasks.
|
||||
var schedMigration = new ClawdDotNet.Core.Tasks.SchedulerTaskMigration(
|
||||
board, taskRepository, loggerFactory);
|
||||
var migratedSched = await schedMigration.RunAsync(instanceConfig.Agents, CancellationToken.None);
|
||||
|
||||
var imported = await board.ImportAllAsync(CancellationToken.None);
|
||||
coreLogger.LogInformation(
|
||||
"Taskboard bereit: {Imported} Aufgabe(n), migriert {Coord} coordination + {Sched} scheduler, {Reset} verwaiste Claims zurückgesetzt",
|
||||
imported, migratedCoord, migratedSched, reset);
|
||||
taskScanner.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
coreLogger.LogWarning(ex, "Taskboard-Reconciliation beim Start fehlgeschlagen");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
coreLogger.LogInformation("AgentEngine und Scanner erstellt, Chat-Verläufe geladen");
|
||||
}
|
||||
else
|
||||
{
|
||||
coreLogger.LogWarning("Kein OpenRouter API-Key konfiguriert – Agenten sind deaktiviert");
|
||||
}
|
||||
|
||||
// ─── 6b. TelegramClient verbinden (Session oder interaktiver Login) ───
|
||||
if (tgClientManager is not null)
|
||||
{
|
||||
tgClientManager.OnLoginCodeRequired += async (prompt) =>
|
||||
{
|
||||
string? code = null;
|
||||
await Task.Run(() =>
|
||||
{
|
||||
code = Microsoft.VisualBasic.Interaction.InputBox(
|
||||
prompt, "Telegram Verifizierung", "");
|
||||
});
|
||||
return code ?? "";
|
||||
};
|
||||
|
||||
tgClientManager.On2FAPasswordRequired += async () =>
|
||||
{
|
||||
string? pw = null;
|
||||
await Task.Run(() =>
|
||||
{
|
||||
pw = Microsoft.VisualBasic.Interaction.InputBox(
|
||||
"Bitte 2FA-Passwort eingeben:", "Telegram 2FA", "");
|
||||
});
|
||||
return pw ?? "";
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
tgClientManager.ConnectAsync(CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
coreLogger.LogError(ex, "Telegram: Login fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 6c. Instanz-Watchdog ───
|
||||
// Entfällt hier ebenfalls. Der Heartbeat ans Deploymentcenter (ein Monitor je
|
||||
// Instanz, mit Gesundheitsprüfungen und angekündigtem Ende) steht in
|
||||
// ClawdDotNet.App/Services/DeploymentcenterService.cs.
|
||||
|
||||
// ─── 7. MainForm starten ───
|
||||
var form = new frm_main(
|
||||
settingsManager,
|
||||
instanceConfig,
|
||||
instancePath,
|
||||
logDir,
|
||||
loggerFactory,
|
||||
toolRegistry,
|
||||
dirManager,
|
||||
agentEngine,
|
||||
taskScanner,
|
||||
stagingService);
|
||||
|
||||
Application.Run(form);
|
||||
|
||||
// ─── 8. Aufräumen ───
|
||||
coreLogger.LogInformation("ClawdDotNet wird beendet");
|
||||
|
||||
if (taskScanner is not null)
|
||||
taskScanner.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
|
||||
if (tgClientManager is not null)
|
||||
tgClientManager.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
|
||||
openRouterClient?.Dispose();
|
||||
loggerFactory.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// Dieser Code wurde von einem Tool generiert.
|
||||
// Laufzeitversion:4.0.30319.42000
|
||||
//
|
||||
// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn
|
||||
// der Code erneut generiert wird.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ClawdDotNet.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw.
|
||||
/// </summary>
|
||||
// Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert
|
||||
// -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert.
|
||||
// Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen
|
||||
// mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ClawdDotNet.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle
|
||||
/// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap add {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("add", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap cog {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("cog", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap messenger {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("messenger", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap plus {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("plus", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap server_add {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("server_add", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap server_go {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("server_go", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="add" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="cog" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\cog.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="messenger" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\messenger.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="plus" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\plus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="server_add" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\server_add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="server_go" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\server_go.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
</root>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,255 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ClawdDotNet.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Überwacht das Logs-Verzeichnis und liefert neue Log-Einträge gefiltert
|
||||
/// an eine RichTextBox. Bereinigt die RichTextBox automatisch wenn sie
|
||||
/// zu voll wird, damit das UI reaktionsfähig bleibt.
|
||||
///
|
||||
/// Struktur: Logs/{Datum}/{Modul}.log
|
||||
/// Log-Format: [{Timestamp}] [{LEVEL}] {Message}
|
||||
/// </summary>
|
||||
public sealed class LiveLogViewerService : IDisposable
|
||||
{
|
||||
private readonly string _logDirectory;
|
||||
private readonly RichTextBox _target;
|
||||
private readonly System.Windows.Forms.Timer _refreshTimer;
|
||||
private readonly int _maxLines;
|
||||
|
||||
// Tracking: pro Datei die letzte gelesene Position
|
||||
private readonly ConcurrentDictionary<string, long> _filePositions = new();
|
||||
|
||||
// Filter
|
||||
private string _moduleFilter = ""; // leer = alle
|
||||
private string _levelFilter = ""; // leer = alle
|
||||
|
||||
private static readonly Regex LevelRegex = new(
|
||||
@"\[(INF|WRN|ERR|DBG)\]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public LiveLogViewerService(string logDirectory, RichTextBox target, int refreshIntervalMs = 500, int maxLines = 2000)
|
||||
{
|
||||
_logDirectory = logDirectory;
|
||||
_target = target;
|
||||
_maxLines = maxLines;
|
||||
|
||||
_refreshTimer = new System.Windows.Forms.Timer { Interval = refreshIntervalMs };
|
||||
_refreshTimer.Tick += OnTimerTick;
|
||||
}
|
||||
|
||||
public void Start() => _refreshTimer.Start();
|
||||
public void Stop() => _refreshTimer.Stop();
|
||||
|
||||
public void SetModuleFilter(string module)
|
||||
{
|
||||
_moduleFilter = module;
|
||||
ClearAndResetPositions();
|
||||
}
|
||||
|
||||
public void SetLevelFilter(string level)
|
||||
{
|
||||
_levelFilter = level;
|
||||
ClearAndResetPositions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt alle erkannten Modul-Namen zurück (basierend auf den vorhandenen .log-Dateien).
|
||||
/// </summary>
|
||||
public List<string> GetAvailableModules()
|
||||
{
|
||||
var modules = new HashSet<string> { "Alle" };
|
||||
|
||||
if (!Directory.Exists(_logDirectory))
|
||||
return modules.ToList();
|
||||
|
||||
// Alle Unterordner (Datum-Ordner) durchsuchen
|
||||
foreach (var dateDir in Directory.GetDirectories(_logDirectory))
|
||||
{
|
||||
foreach (var logFile in Directory.GetFiles(dateDir, "*.log"))
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(logFile);
|
||||
modules.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return modules.OrderBy(m => m == "Alle" ? "" : m).ToList();
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
ReadNewEntries();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Logging-Viewer darf niemals das UI crashen
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadNewEntries()
|
||||
{
|
||||
if (!Directory.Exists(_logDirectory))
|
||||
return;
|
||||
|
||||
// Heutiges Datum-Verzeichnis (und ggf. gestriges für Logs um Mitternacht)
|
||||
var today = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var todayDir = Path.Combine(_logDirectory, today);
|
||||
|
||||
if (!Directory.Exists(todayDir))
|
||||
return;
|
||||
|
||||
var logFiles = Directory.GetFiles(todayDir, "*.log");
|
||||
var newLines = new List<(DateTime time, string line, string module)>();
|
||||
|
||||
foreach (var filePath in logFiles)
|
||||
{
|
||||
var moduleName = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
// Modul-Filter
|
||||
if (!string.IsNullOrEmpty(_moduleFilter) && _moduleFilter != "Alle"
|
||||
&& !string.Equals(moduleName, _moduleFilter, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var lastPos = _filePositions.GetOrAdd(filePath, 0L);
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
|
||||
if (fs.Length < lastPos)
|
||||
{
|
||||
// Datei wurde rotiert/gekürzt
|
||||
lastPos = 0;
|
||||
}
|
||||
|
||||
if (fs.Length == lastPos)
|
||||
continue;
|
||||
|
||||
fs.Seek(lastPos, SeekOrigin.Begin);
|
||||
using var reader = new StreamReader(fs);
|
||||
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Level-Filter
|
||||
if (!string.IsNullOrEmpty(_levelFilter) && _levelFilter != "Alle")
|
||||
{
|
||||
if (!PassesLevelFilter(line))
|
||||
continue;
|
||||
}
|
||||
|
||||
newLines.Add((DateTime.Now, $"[{moduleName}] {line}", moduleName));
|
||||
}
|
||||
|
||||
_filePositions[filePath] = fs.Position;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Datei wird gerade geschrieben - nächstes Mal versuchen
|
||||
}
|
||||
}
|
||||
|
||||
if (newLines.Count == 0)
|
||||
return;
|
||||
|
||||
// In UI schreiben
|
||||
AppendToRichTextBox(newLines);
|
||||
}
|
||||
|
||||
private bool PassesLevelFilter(string line)
|
||||
{
|
||||
var match = LevelRegex.Match(line);
|
||||
if (!match.Success)
|
||||
return true; // Unbekanntes Format durchlassen
|
||||
|
||||
var level = match.Groups[1].Value;
|
||||
return _levelFilter switch
|
||||
{
|
||||
"Info" => level is "INF" or "WRN" or "ERR",
|
||||
"Warn" => level is "WRN" or "ERR",
|
||||
"Error" => level is "ERR",
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private void AppendToRichTextBox(List<(DateTime time, string line, string module)> lines)
|
||||
{
|
||||
if (_target.IsDisposed || !_target.IsHandleCreated)
|
||||
return;
|
||||
|
||||
_target.BeginInvoke(() =>
|
||||
{
|
||||
_target.SuspendLayout();
|
||||
|
||||
foreach (var (_, line, module) in lines)
|
||||
{
|
||||
var color = GetColorForLine(line);
|
||||
_target.SelectionStart = _target.TextLength;
|
||||
_target.SelectionLength = 0;
|
||||
_target.SelectionColor = color;
|
||||
_target.AppendText(line + Environment.NewLine);
|
||||
}
|
||||
|
||||
// Bereinigung: wenn zu viele Zeilen, die ältesten entfernen
|
||||
TrimIfNeeded();
|
||||
|
||||
// Auto-Scroll zum Ende
|
||||
_target.SelectionStart = _target.TextLength;
|
||||
_target.ScrollToCaret();
|
||||
|
||||
_target.ResumeLayout();
|
||||
});
|
||||
}
|
||||
|
||||
private static Color GetColorForLine(string line)
|
||||
{
|
||||
if (line.Contains("[ERR]"))
|
||||
return Color.Red;
|
||||
if (line.Contains("[WRN]"))
|
||||
return Color.Orange;
|
||||
if (line.Contains("[DBG]"))
|
||||
return Color.Gray;
|
||||
return Color.LightGreen; // INF
|
||||
}
|
||||
|
||||
private void TrimIfNeeded()
|
||||
{
|
||||
if (_target.Lines.Length <= _maxLines)
|
||||
return;
|
||||
|
||||
// Die älteste Hälfte entfernen
|
||||
var removeCount = _maxLines / 2;
|
||||
var removeEndIndex = _target.GetFirstCharIndexFromLine(removeCount);
|
||||
|
||||
if (removeEndIndex <= 0)
|
||||
return;
|
||||
|
||||
_target.SelectionStart = 0;
|
||||
_target.SelectionLength = removeEndIndex;
|
||||
_target.SelectedText = $"--- {removeCount} ältere Zeilen entfernt ---{Environment.NewLine}";
|
||||
}
|
||||
|
||||
private void ClearAndResetPositions()
|
||||
{
|
||||
_filePositions.Clear();
|
||||
|
||||
if (_target.IsHandleCreated && !_target.IsDisposed)
|
||||
{
|
||||
_target.BeginInvoke(() =>
|
||||
{
|
||||
_target.Clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_refreshTimer.Stop();
|
||||
_refreshTimer.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
namespace ClawdDotNet.UI;
|
||||
|
||||
partial class BackupPanel
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && components is not null)
|
||||
components.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
layoutRoot = new TableLayoutPanel();
|
||||
grp_create = new GroupBox();
|
||||
layoutCreate = new TableLayoutPanel();
|
||||
label_target = new Label();
|
||||
txt_targetFolder = new TextBox();
|
||||
btn_browseFolder = new Button();
|
||||
label_secrets = new Label();
|
||||
panel_secretMode = new FlowLayoutPanel();
|
||||
rb_secretsExclude = new RadioButton();
|
||||
rb_secretsPassphrase = new RadioButton();
|
||||
label_passphrase = new Label();
|
||||
txt_passphrase = new TextBox();
|
||||
label_passphraseRepeat = new Label();
|
||||
txt_passphraseRepeat = new TextBox();
|
||||
label_scope = new Label();
|
||||
panel_scope = new FlowLayoutPanel();
|
||||
chk_includeChatHistory = new CheckBox();
|
||||
chk_includeLogs = new CheckBox();
|
||||
panel_createActions = new FlowLayoutPanel();
|
||||
btn_createBackup = new Button();
|
||||
label_createStatus = new Label();
|
||||
|
||||
grp_auto = new GroupBox();
|
||||
layoutAuto = new TableLayoutPanel();
|
||||
chk_autoEnabled = new CheckBox();
|
||||
dtp_autoTime = new DateTimePicker();
|
||||
label_keepCount = new Label();
|
||||
num_keepCount = new NumericUpDown();
|
||||
label_autoHint = new Label();
|
||||
|
||||
grp_existing = new GroupBox();
|
||||
layoutExisting = new TableLayoutPanel();
|
||||
lv_backups = new ListView();
|
||||
col_file = new ColumnHeader();
|
||||
col_created = new ColumnHeader();
|
||||
col_instance = new ColumnHeader();
|
||||
col_size = new ColumnHeader();
|
||||
col_secrets = new ColumnHeader();
|
||||
panel_existingActions = new FlowLayoutPanel();
|
||||
btn_refresh = new Button();
|
||||
btn_restore = new Button();
|
||||
btn_showInFolder = new Button();
|
||||
btn_delete = new Button();
|
||||
|
||||
layoutRoot.SuspendLayout();
|
||||
grp_create.SuspendLayout();
|
||||
layoutCreate.SuspendLayout();
|
||||
panel_secretMode.SuspendLayout();
|
||||
panel_scope.SuspendLayout();
|
||||
panel_createActions.SuspendLayout();
|
||||
grp_auto.SuspendLayout();
|
||||
layoutAuto.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)num_keepCount).BeginInit();
|
||||
grp_existing.SuspendLayout();
|
||||
layoutExisting.SuspendLayout();
|
||||
panel_existingActions.SuspendLayout();
|
||||
SuspendLayout();
|
||||
|
||||
// ─── Wurzel ───
|
||||
layoutRoot.ColumnCount = 1;
|
||||
layoutRoot.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
layoutRoot.Dock = DockStyle.Fill;
|
||||
layoutRoot.Name = "layoutRoot";
|
||||
layoutRoot.Padding = new Padding(12);
|
||||
layoutRoot.RowCount = 3;
|
||||
layoutRoot.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layoutRoot.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layoutRoot.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
layoutRoot.Controls.Add(grp_create, 0, 0);
|
||||
layoutRoot.Controls.Add(grp_auto, 0, 1);
|
||||
layoutRoot.Controls.Add(grp_existing, 0, 2);
|
||||
|
||||
// ═══ Sicherung erstellen ═══
|
||||
grp_create.AutoSize = true;
|
||||
grp_create.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
grp_create.Controls.Add(layoutCreate);
|
||||
grp_create.Dock = DockStyle.Fill;
|
||||
grp_create.Name = "grp_create";
|
||||
grp_create.Padding = new Padding(10);
|
||||
grp_create.TabIndex = 0;
|
||||
grp_create.TabStop = false;
|
||||
grp_create.Text = "Sicherung erstellen";
|
||||
|
||||
layoutCreate.AutoSize = true;
|
||||
layoutCreate.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
layoutCreate.ColumnCount = 3;
|
||||
layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
layoutCreate.Dock = DockStyle.Fill;
|
||||
layoutCreate.Name = "layoutCreate";
|
||||
layoutCreate.RowCount = 6;
|
||||
for (var i = 0; i < 6; i++)
|
||||
layoutCreate.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
label_target.Anchor = AnchorStyles.Left;
|
||||
label_target.AutoSize = true;
|
||||
label_target.Name = "label_target";
|
||||
label_target.Text = "Zielordner:";
|
||||
|
||||
txt_targetFolder.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
txt_targetFolder.Name = "txt_targetFolder";
|
||||
|
||||
btn_browseFolder.AutoSize = true;
|
||||
btn_browseFolder.Name = "btn_browseFolder";
|
||||
btn_browseFolder.Text = "Durchsuchen…";
|
||||
btn_browseFolder.UseVisualStyleBackColor = true;
|
||||
|
||||
label_secrets.Anchor = AnchorStyles.Left;
|
||||
label_secrets.AutoSize = true;
|
||||
label_secrets.Name = "label_secrets";
|
||||
label_secrets.Text = "Zugangsdaten:";
|
||||
|
||||
panel_secretMode.AutoSize = true;
|
||||
panel_secretMode.Controls.Add(rb_secretsExclude);
|
||||
panel_secretMode.Controls.Add(rb_secretsPassphrase);
|
||||
panel_secretMode.Margin = new Padding(0);
|
||||
panel_secretMode.Name = "panel_secretMode";
|
||||
panel_secretMode.WrapContents = false;
|
||||
|
||||
rb_secretsExclude.AutoSize = true;
|
||||
rb_secretsExclude.Checked = true;
|
||||
rb_secretsExclude.Name = "rb_secretsExclude";
|
||||
rb_secretsExclude.TabStop = true;
|
||||
rb_secretsExclude.Text = "nicht mitsichern";
|
||||
rb_secretsExclude.UseVisualStyleBackColor = true;
|
||||
|
||||
rb_secretsPassphrase.AutoSize = true;
|
||||
rb_secretsPassphrase.Name = "rb_secretsPassphrase";
|
||||
rb_secretsPassphrase.Text = "mit Passphrase schützen";
|
||||
rb_secretsPassphrase.UseVisualStyleBackColor = true;
|
||||
|
||||
label_passphrase.Anchor = AnchorStyles.Left;
|
||||
label_passphrase.AutoSize = true;
|
||||
label_passphrase.Name = "label_passphrase";
|
||||
label_passphrase.Text = "Passphrase:";
|
||||
|
||||
txt_passphrase.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
txt_passphrase.Name = "txt_passphrase";
|
||||
txt_passphrase.UseSystemPasswordChar = true;
|
||||
|
||||
label_passphraseRepeat.Anchor = AnchorStyles.Left;
|
||||
label_passphraseRepeat.AutoSize = true;
|
||||
label_passphraseRepeat.Name = "label_passphraseRepeat";
|
||||
label_passphraseRepeat.Text = "Wiederholen:";
|
||||
|
||||
txt_passphraseRepeat.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
txt_passphraseRepeat.Name = "txt_passphraseRepeat";
|
||||
txt_passphraseRepeat.UseSystemPasswordChar = true;
|
||||
|
||||
label_scope.Anchor = AnchorStyles.Left;
|
||||
label_scope.AutoSize = true;
|
||||
label_scope.Name = "label_scope";
|
||||
label_scope.Text = "Umfang:";
|
||||
|
||||
panel_scope.AutoSize = true;
|
||||
panel_scope.Controls.Add(chk_includeChatHistory);
|
||||
panel_scope.Controls.Add(chk_includeLogs);
|
||||
panel_scope.Margin = new Padding(0);
|
||||
panel_scope.Name = "panel_scope";
|
||||
panel_scope.WrapContents = false;
|
||||
|
||||
chk_includeChatHistory.AutoSize = true;
|
||||
chk_includeChatHistory.Checked = true;
|
||||
chk_includeChatHistory.CheckState = CheckState.Checked;
|
||||
chk_includeChatHistory.Name = "chk_includeChatHistory";
|
||||
chk_includeChatHistory.Text = "Chatverläufe";
|
||||
chk_includeChatHistory.UseVisualStyleBackColor = true;
|
||||
|
||||
chk_includeLogs.AutoSize = true;
|
||||
chk_includeLogs.Name = "chk_includeLogs";
|
||||
chk_includeLogs.Text = "Protokolle";
|
||||
chk_includeLogs.UseVisualStyleBackColor = true;
|
||||
|
||||
panel_createActions.AutoSize = true;
|
||||
panel_createActions.Controls.Add(btn_createBackup);
|
||||
panel_createActions.Controls.Add(label_createStatus);
|
||||
panel_createActions.Margin = new Padding(0, 8, 0, 0);
|
||||
panel_createActions.Name = "panel_createActions";
|
||||
panel_createActions.WrapContents = false;
|
||||
|
||||
btn_createBackup.AutoSize = true;
|
||||
btn_createBackup.Name = "btn_createBackup";
|
||||
btn_createBackup.Padding = new Padding(8, 2, 8, 2);
|
||||
btn_createBackup.Text = "Sicherung jetzt erstellen";
|
||||
btn_createBackup.UseVisualStyleBackColor = true;
|
||||
|
||||
label_createStatus.Anchor = AnchorStyles.Left;
|
||||
label_createStatus.AutoSize = true;
|
||||
label_createStatus.Margin = new Padding(12, 8, 3, 0);
|
||||
label_createStatus.Name = "label_createStatus";
|
||||
|
||||
layoutCreate.Controls.Add(label_target, 0, 0);
|
||||
layoutCreate.Controls.Add(txt_targetFolder, 1, 0);
|
||||
layoutCreate.Controls.Add(btn_browseFolder, 2, 0);
|
||||
layoutCreate.Controls.Add(label_secrets, 0, 1);
|
||||
layoutCreate.Controls.Add(panel_secretMode, 1, 1);
|
||||
layoutCreate.Controls.Add(label_passphrase, 0, 2);
|
||||
layoutCreate.Controls.Add(txt_passphrase, 1, 2);
|
||||
layoutCreate.Controls.Add(label_passphraseRepeat, 0, 3);
|
||||
layoutCreate.Controls.Add(txt_passphraseRepeat, 1, 3);
|
||||
layoutCreate.Controls.Add(label_scope, 0, 4);
|
||||
layoutCreate.Controls.Add(panel_scope, 1, 4);
|
||||
layoutCreate.Controls.Add(panel_createActions, 1, 5);
|
||||
|
||||
// ═══ Automatik ═══
|
||||
grp_auto.AutoSize = true;
|
||||
grp_auto.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
grp_auto.Controls.Add(layoutAuto);
|
||||
grp_auto.Dock = DockStyle.Fill;
|
||||
grp_auto.Margin = new Padding(3, 10, 3, 3);
|
||||
grp_auto.Name = "grp_auto";
|
||||
grp_auto.Padding = new Padding(10);
|
||||
grp_auto.TabIndex = 1;
|
||||
grp_auto.TabStop = false;
|
||||
grp_auto.Text = "Automatische Sicherung";
|
||||
|
||||
layoutAuto.AutoSize = true;
|
||||
layoutAuto.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
layoutAuto.ColumnCount = 5;
|
||||
for (var i = 0; i < 4; i++)
|
||||
layoutAuto.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
layoutAuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
layoutAuto.Dock = DockStyle.Fill;
|
||||
layoutAuto.Name = "layoutAuto";
|
||||
layoutAuto.RowCount = 2;
|
||||
layoutAuto.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layoutAuto.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
chk_autoEnabled.Anchor = AnchorStyles.Left;
|
||||
chk_autoEnabled.AutoSize = true;
|
||||
chk_autoEnabled.Name = "chk_autoEnabled";
|
||||
chk_autoEnabled.Text = "täglich sichern um";
|
||||
chk_autoEnabled.UseVisualStyleBackColor = true;
|
||||
|
||||
dtp_autoTime.Anchor = AnchorStyles.Left;
|
||||
dtp_autoTime.Format = DateTimePickerFormat.Time;
|
||||
dtp_autoTime.Name = "dtp_autoTime";
|
||||
dtp_autoTime.ShowUpDown = true;
|
||||
dtp_autoTime.Width = 90;
|
||||
|
||||
label_keepCount.Anchor = AnchorStyles.Left;
|
||||
label_keepCount.AutoSize = true;
|
||||
label_keepCount.Margin = new Padding(24, 6, 3, 0);
|
||||
label_keepCount.Name = "label_keepCount";
|
||||
label_keepCount.Text = "Aufbewahren:";
|
||||
|
||||
num_keepCount.Anchor = AnchorStyles.Left;
|
||||
num_keepCount.Maximum = 999;
|
||||
num_keepCount.Name = "num_keepCount";
|
||||
num_keepCount.Width = 70;
|
||||
|
||||
label_autoHint.Anchor = AnchorStyles.Left;
|
||||
label_autoHint.AutoSize = true;
|
||||
label_autoHint.ForeColor = SystemColors.GrayText;
|
||||
label_autoHint.Name = "label_autoHint";
|
||||
label_autoHint.Text = "Die Automatik sichert ohne Zugangsdaten — eine gespeicherte Passphrase "
|
||||
+ "neben den Sicherungen wäre wirkungslos.";
|
||||
|
||||
layoutAuto.Controls.Add(chk_autoEnabled, 0, 0);
|
||||
layoutAuto.Controls.Add(dtp_autoTime, 1, 0);
|
||||
layoutAuto.Controls.Add(label_keepCount, 2, 0);
|
||||
layoutAuto.Controls.Add(num_keepCount, 3, 0);
|
||||
layoutAuto.Controls.Add(label_autoHint, 0, 1);
|
||||
layoutAuto.SetColumnSpan(label_autoHint, 5);
|
||||
|
||||
// ═══ Vorhandene Sicherungen ═══
|
||||
grp_existing.Controls.Add(layoutExisting);
|
||||
grp_existing.Dock = DockStyle.Fill;
|
||||
grp_existing.Margin = new Padding(3, 10, 3, 3);
|
||||
grp_existing.Name = "grp_existing";
|
||||
grp_existing.Padding = new Padding(10);
|
||||
grp_existing.TabIndex = 2;
|
||||
grp_existing.TabStop = false;
|
||||
grp_existing.Text = "Vorhandene Sicherungen";
|
||||
|
||||
layoutExisting.ColumnCount = 1;
|
||||
layoutExisting.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
layoutExisting.Dock = DockStyle.Fill;
|
||||
layoutExisting.Name = "layoutExisting";
|
||||
layoutExisting.RowCount = 2;
|
||||
layoutExisting.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
layoutExisting.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layoutExisting.Controls.Add(lv_backups, 0, 0);
|
||||
layoutExisting.Controls.Add(panel_existingActions, 0, 1);
|
||||
|
||||
lv_backups.Columns.AddRange(new[] { col_file, col_created, col_instance, col_size, col_secrets });
|
||||
lv_backups.Dock = DockStyle.Fill;
|
||||
lv_backups.FullRowSelect = true;
|
||||
lv_backups.MultiSelect = false;
|
||||
lv_backups.Name = "lv_backups";
|
||||
lv_backups.UseCompatibleStateImageBehavior = false;
|
||||
lv_backups.View = View.Details;
|
||||
|
||||
col_file.Text = "Datei";
|
||||
col_file.Width = 420;
|
||||
col_created.Text = "Erstellt";
|
||||
col_created.Width = 150;
|
||||
col_instance.Text = "Instanz";
|
||||
col_instance.Width = 180;
|
||||
col_size.Text = "Größe";
|
||||
col_size.Width = 100;
|
||||
col_secrets.Text = "Zugangsdaten";
|
||||
col_secrets.Width = 140;
|
||||
|
||||
panel_existingActions.AutoSize = true;
|
||||
panel_existingActions.Controls.Add(btn_refresh);
|
||||
panel_existingActions.Controls.Add(btn_restore);
|
||||
panel_existingActions.Controls.Add(btn_showInFolder);
|
||||
panel_existingActions.Controls.Add(btn_delete);
|
||||
panel_existingActions.Margin = new Padding(0, 8, 0, 0);
|
||||
panel_existingActions.Name = "panel_existingActions";
|
||||
panel_existingActions.WrapContents = false;
|
||||
|
||||
btn_refresh.AutoSize = true;
|
||||
btn_refresh.Name = "btn_refresh";
|
||||
btn_refresh.Text = "Aktualisieren";
|
||||
btn_refresh.UseVisualStyleBackColor = true;
|
||||
|
||||
btn_restore.AutoSize = true;
|
||||
btn_restore.Name = "btn_restore";
|
||||
btn_restore.Padding = new Padding(8, 2, 8, 2);
|
||||
btn_restore.Text = "Wiederherstellen…";
|
||||
btn_restore.UseVisualStyleBackColor = true;
|
||||
|
||||
btn_showInFolder.AutoSize = true;
|
||||
btn_showInFolder.Name = "btn_showInFolder";
|
||||
btn_showInFolder.Text = "Im Ordner zeigen";
|
||||
btn_showInFolder.UseVisualStyleBackColor = true;
|
||||
|
||||
btn_delete.AutoSize = true;
|
||||
btn_delete.Name = "btn_delete";
|
||||
btn_delete.Text = "Löschen";
|
||||
btn_delete.UseVisualStyleBackColor = true;
|
||||
|
||||
// ─── Steuerelement ───
|
||||
AutoScaleDimensions = new SizeF(10F, 25F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(layoutRoot);
|
||||
Name = "BackupPanel";
|
||||
Size = new Size(1200, 800);
|
||||
|
||||
layoutRoot.ResumeLayout(false);
|
||||
layoutRoot.PerformLayout();
|
||||
grp_create.ResumeLayout(false);
|
||||
grp_create.PerformLayout();
|
||||
layoutCreate.ResumeLayout(false);
|
||||
layoutCreate.PerformLayout();
|
||||
panel_secretMode.ResumeLayout(false);
|
||||
panel_secretMode.PerformLayout();
|
||||
panel_scope.ResumeLayout(false);
|
||||
panel_scope.PerformLayout();
|
||||
panel_createActions.ResumeLayout(false);
|
||||
panel_createActions.PerformLayout();
|
||||
grp_auto.ResumeLayout(false);
|
||||
grp_auto.PerformLayout();
|
||||
layoutAuto.ResumeLayout(false);
|
||||
layoutAuto.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)num_keepCount).EndInit();
|
||||
grp_existing.ResumeLayout(false);
|
||||
layoutExisting.ResumeLayout(false);
|
||||
layoutExisting.PerformLayout();
|
||||
panel_existingActions.ResumeLayout(false);
|
||||
panel_existingActions.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
private TableLayoutPanel layoutRoot;
|
||||
|
||||
private GroupBox grp_create;
|
||||
private TableLayoutPanel layoutCreate;
|
||||
private Label label_target;
|
||||
private TextBox txt_targetFolder;
|
||||
private Button btn_browseFolder;
|
||||
private Label label_secrets;
|
||||
private FlowLayoutPanel panel_secretMode;
|
||||
private RadioButton rb_secretsExclude;
|
||||
private RadioButton rb_secretsPassphrase;
|
||||
private Label label_passphrase;
|
||||
private TextBox txt_passphrase;
|
||||
private Label label_passphraseRepeat;
|
||||
private TextBox txt_passphraseRepeat;
|
||||
private Label label_scope;
|
||||
private FlowLayoutPanel panel_scope;
|
||||
private CheckBox chk_includeChatHistory;
|
||||
private CheckBox chk_includeLogs;
|
||||
private FlowLayoutPanel panel_createActions;
|
||||
private Button btn_createBackup;
|
||||
private Label label_createStatus;
|
||||
|
||||
private GroupBox grp_auto;
|
||||
private TableLayoutPanel layoutAuto;
|
||||
private CheckBox chk_autoEnabled;
|
||||
private DateTimePicker dtp_autoTime;
|
||||
private Label label_keepCount;
|
||||
private NumericUpDown num_keepCount;
|
||||
private Label label_autoHint;
|
||||
|
||||
private GroupBox grp_existing;
|
||||
private TableLayoutPanel layoutExisting;
|
||||
private ListView lv_backups;
|
||||
private ColumnHeader col_file;
|
||||
private ColumnHeader col_created;
|
||||
private ColumnHeader col_instance;
|
||||
private ColumnHeader col_size;
|
||||
private ColumnHeader col_secrets;
|
||||
private FlowLayoutPanel panel_existingActions;
|
||||
private Button btn_refresh;
|
||||
private Button btn_restore;
|
||||
private Button btn_showInFolder;
|
||||
private Button btn_delete;
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
using ClawdDotNet.Core.Backup;
|
||||
using ClawdDotNet.Models;
|
||||
using ClawdDotNet.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Bedienoberfläche für Sicherung und Wiederherstellung einer Instanz.
|
||||
///
|
||||
/// Zwei Entscheidungen prägen das Verhalten:
|
||||
///
|
||||
/// Die Passphrase wird nirgends gespeichert. Läge sie neben den Sicherungen, wäre die
|
||||
/// Verschlüsselung wirkungslos. Deshalb muss sie bei jeder geschützten Sicherung neu
|
||||
/// eingegeben werden, und die Automatik sichert ohne Zugangsdaten.
|
||||
///
|
||||
/// Wiederhergestellt wird standardmäßig in einen NEUEN Ordner, nicht über die laufende
|
||||
/// Instanz. Diese hält Chatverläufe im Speicher und die Datenbank geöffnet — ein
|
||||
/// Überschreiben im Betrieb würde teils sofort wieder überschrieben und teils
|
||||
/// scheitern.
|
||||
/// </summary>
|
||||
public sealed partial class BackupPanel : UserControl
|
||||
{
|
||||
private readonly BackupService _service = new();
|
||||
|
||||
private SettingsManager? _settingsManager;
|
||||
private ILogger? _logger;
|
||||
private string _instanceDir = "";
|
||||
private string _instanceName = "";
|
||||
private bool _loading;
|
||||
|
||||
public BackupPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
WireEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aktualisiert die Liste von außen — etwa nachdem die Automatik gesichert hat.
|
||||
/// </summary>
|
||||
public void RefreshFromOutside()
|
||||
{
|
||||
if (!IsHandleCreated || IsDisposed)
|
||||
return;
|
||||
|
||||
RefreshBackupList();
|
||||
}
|
||||
|
||||
/// <summary>Wird vom Hauptfenster nach dem Laden der Instanz aufgerufen.</summary>
|
||||
public void Initialize(string instanceDir, string instanceName,
|
||||
SettingsManager settingsManager, ILogger logger)
|
||||
{
|
||||
_instanceDir = instanceDir;
|
||||
_instanceName = instanceName;
|
||||
_settingsManager = settingsManager;
|
||||
_logger = logger;
|
||||
|
||||
LoadSettings();
|
||||
UpdateEnabledState();
|
||||
RefreshBackupList();
|
||||
}
|
||||
|
||||
private void WireEvents()
|
||||
{
|
||||
btn_browseFolder.Click += OnBrowseFolder;
|
||||
btn_createBackup.Click += async (_, _) => await CreateBackupAsync();
|
||||
btn_refresh.Click += (_, _) => RefreshBackupList();
|
||||
btn_restore.Click += async (_, _) => await RestoreSelectedAsync();
|
||||
btn_showInFolder.Click += (_, _) => ShowSelectedInFolder();
|
||||
btn_delete.Click += (_, _) => DeleteSelected();
|
||||
|
||||
rb_secretsExclude.CheckedChanged += (_, _) => UpdateEnabledState();
|
||||
rb_secretsPassphrase.CheckedChanged += (_, _) => UpdateEnabledState();
|
||||
|
||||
chk_autoEnabled.CheckedChanged += (_, _) => { UpdateEnabledState(); SaveSettings(); };
|
||||
dtp_autoTime.ValueChanged += (_, _) => SaveSettings();
|
||||
num_keepCount.ValueChanged += (_, _) => SaveSettings();
|
||||
txt_targetFolder.Leave += (_, _) => SaveSettings();
|
||||
|
||||
lv_backups.SelectedIndexChanged += (_, _) => UpdateEnabledState();
|
||||
lv_backups.DoubleClick += async (_, _) => await RestoreSelectedAsync();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Einstellungen
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
if (_settingsManager is null) return;
|
||||
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var settings = _settingsManager.AppSettings;
|
||||
|
||||
txt_targetFolder.Text = Path.GetFullPath(settings.BackupDirectory);
|
||||
chk_autoEnabled.Checked = settings.AutoBackupEnabled;
|
||||
num_keepCount.Value = Math.Clamp(settings.BackupKeepCount, 0, 999);
|
||||
|
||||
dtp_autoTime.Value = TimeSpan.TryParse(settings.AutoBackupTime, out var time)
|
||||
? DateTime.Today.Add(time)
|
||||
: DateTime.Today.AddHours(3);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
if (_loading || _settingsManager is null) return;
|
||||
|
||||
var settings = _settingsManager.AppSettings;
|
||||
settings.BackupDirectory = txt_targetFolder.Text.Trim();
|
||||
settings.AutoBackupEnabled = chk_autoEnabled.Checked;
|
||||
settings.AutoBackupTime = dtp_autoTime.Value.ToString("HH:mm");
|
||||
settings.BackupKeepCount = (int)num_keepCount.Value;
|
||||
|
||||
_settingsManager.Save();
|
||||
}
|
||||
|
||||
private void UpdateEnabledState()
|
||||
{
|
||||
var withPassphrase = rb_secretsPassphrase.Checked;
|
||||
label_passphrase.Enabled = withPassphrase;
|
||||
txt_passphrase.Enabled = withPassphrase;
|
||||
label_passphraseRepeat.Enabled = withPassphrase;
|
||||
txt_passphraseRepeat.Enabled = withPassphrase;
|
||||
|
||||
dtp_autoTime.Enabled = chk_autoEnabled.Checked;
|
||||
num_keepCount.Enabled = chk_autoEnabled.Checked;
|
||||
|
||||
var hasSelection = lv_backups.SelectedItems.Count > 0;
|
||||
btn_restore.Enabled = hasSelection;
|
||||
btn_showInFolder.Enabled = hasSelection;
|
||||
btn_delete.Enabled = hasSelection;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Sichern
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void OnBrowseFolder(object? sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new FolderBrowserDialog
|
||||
{
|
||||
Description = "Ordner für Sicherungen wählen",
|
||||
UseDescriptionForTitle = true,
|
||||
SelectedPath = Directory.Exists(txt_targetFolder.Text)
|
||||
? txt_targetFolder.Text
|
||||
: Environment.CurrentDirectory
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
txt_targetFolder.Text = dialog.SelectedPath;
|
||||
SaveSettings();
|
||||
RefreshBackupList();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateBackupAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_instanceDir))
|
||||
return;
|
||||
|
||||
var options = BuildOptions();
|
||||
if (options is null)
|
||||
return; // Eingabefehler, bereits gemeldet
|
||||
|
||||
var folder = txt_targetFolder.Text.Trim();
|
||||
var file = Path.Combine(folder, BuildFileName());
|
||||
|
||||
btn_createBackup.Enabled = false;
|
||||
SetStatus("Sicherung läuft…", SystemColors.GrayText);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Task.Run(() => _service.CreateAsync(_instanceDir, file, options));
|
||||
|
||||
SetStatus($"Fertig: {Path.GetFileName(result.ZipPath)} ({FormatSize(result.SizeBytes)})",
|
||||
Color.FromArgb(0, 128, 0));
|
||||
|
||||
_logger?.LogInformation("Sicherung erstellt: {Path} ({Size} Bytes)",
|
||||
result.ZipPath, result.SizeBytes);
|
||||
|
||||
txt_passphrase.Clear();
|
||||
txt_passphraseRepeat.Clear();
|
||||
|
||||
ApplyRotation(folder);
|
||||
RefreshBackupList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus("Fehlgeschlagen", Color.Firebrick);
|
||||
_logger?.LogError(ex, "Sicherung fehlgeschlagen");
|
||||
MessageBox.Show(this, $"Die Sicherung ist fehlgeschlagen:\n\n{ex.Message}",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
btn_createBackup.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Liest die Eingaben. Gibt null zurück, wenn etwas fehlt.</summary>
|
||||
private BackupOptions? BuildOptions()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(txt_targetFolder.Text))
|
||||
{
|
||||
MessageBox.Show(this, "Bitte einen Zielordner angeben.",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!rb_secretsPassphrase.Checked)
|
||||
{
|
||||
return new BackupOptions
|
||||
{
|
||||
Secrets = SecretMode.Exclude,
|
||||
IncludeLogs = chk_includeLogs.Checked,
|
||||
IncludeChatHistory = chk_includeChatHistory.Checked
|
||||
};
|
||||
}
|
||||
|
||||
var passphrase = txt_passphrase.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(passphrase))
|
||||
{
|
||||
MessageBox.Show(this, "Bitte eine Passphrase eingeben.",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
txt_passphrase.Focus();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (passphrase != txt_passphraseRepeat.Text)
|
||||
{
|
||||
MessageBox.Show(this, "Die beiden Passphrasen stimmen nicht überein.",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
txt_passphraseRepeat.Focus();
|
||||
txt_passphraseRepeat.SelectAll();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Eine vergessene Passphrase bedeutet unwiederbringlich verlorene Zugangsdaten.
|
||||
var answer = MessageBox.Show(this,
|
||||
"Ohne diese Passphrase lassen sich die Zugangsdaten in der Sicherung " +
|
||||
"nicht wiederherstellen. Es gibt keinen Ersatzweg.\n\n" +
|
||||
"Passphrase sicher notiert?",
|
||||
"Passphrase bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
||||
|
||||
if (answer != DialogResult.Yes)
|
||||
return null;
|
||||
|
||||
return new BackupOptions
|
||||
{
|
||||
Secrets = SecretMode.Passphrase,
|
||||
Passphrase = passphrase,
|
||||
IncludeLogs = chk_includeLogs.Checked,
|
||||
IncludeChatHistory = chk_includeChatHistory.Checked
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildFileName()
|
||||
{
|
||||
var safeName = string.Concat(
|
||||
(_instanceName.Length > 0 ? _instanceName : "Instanz")
|
||||
.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
|
||||
return $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Liste
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void RefreshBackupList()
|
||||
{
|
||||
lv_backups.BeginUpdate();
|
||||
lv_backups.Items.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var folder = txt_targetFolder.Text.Trim();
|
||||
if (!Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
var files = new DirectoryInfo(folder)
|
||||
.GetFiles("*.zip")
|
||||
.OrderByDescending(f => f.LastWriteTime);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var item = new ListViewItem(file.Name) { Tag = file.FullName };
|
||||
item.SubItems.Add(file.LastWriteTime.ToString("g"));
|
||||
|
||||
// Das Manifest verrät Instanz und ob Zugangsdaten enthalten sind.
|
||||
try
|
||||
{
|
||||
var manifest = _service.InspectAsync(file.FullName).GetAwaiter().GetResult();
|
||||
item.SubItems.Add(manifest.InstanceName);
|
||||
item.SubItems.Add(FormatSize(file.Length));
|
||||
item.SubItems.Add(manifest.HasSecrets
|
||||
? $"ja ({manifest.SecretCount}, Passphrase)"
|
||||
: "nein");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fremde oder beschädigte Archive trotzdem anzeigen — der Benutzer
|
||||
// soll sehen, was im Ordner liegt.
|
||||
item.SubItems.Add("—");
|
||||
item.SubItems.Add(FormatSize(file.Length));
|
||||
item.SubItems.Add("unlesbar");
|
||||
item.ForeColor = SystemColors.GrayText;
|
||||
}
|
||||
|
||||
lv_backups.Items.Add(item);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lv_backups.EndUpdate();
|
||||
UpdateEnabledState();
|
||||
}
|
||||
}
|
||||
|
||||
private string? SelectedBackupPath
|
||||
=> lv_backups.SelectedItems.Count > 0
|
||||
? lv_backups.SelectedItems[0].Tag as string
|
||||
: null;
|
||||
|
||||
private void ShowSelectedInFolder()
|
||||
{
|
||||
if (SelectedBackupPath is not { } path) return;
|
||||
|
||||
ClawdDotNet.Core.Storage.SystemShell.RevealFile(path);
|
||||
}
|
||||
|
||||
private void DeleteSelected()
|
||||
{
|
||||
if (SelectedBackupPath is not { } path) return;
|
||||
|
||||
var answer = MessageBox.Show(this,
|
||||
$"Sicherung endgültig löschen?\n\n{Path.GetFileName(path)}",
|
||||
"Löschen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
||||
|
||||
if (answer != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
RefreshBackupList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, $"Konnte nicht gelöscht werden:\n\n{ex.Message}",
|
||||
"Löschen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Wiederherstellen
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private async Task RestoreSelectedAsync()
|
||||
{
|
||||
if (SelectedBackupPath is not { } zipPath) return;
|
||||
|
||||
BackupManifest manifest;
|
||||
try
|
||||
{
|
||||
manifest = await _service.InspectAsync(zipPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, $"Die Sicherung ist nicht lesbar:\n\n{ex.Message}",
|
||||
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
using var dialog = new frm_RestoreBackup(manifest, SuggestRestoreTarget(manifest));
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var options = new RestoreOptions
|
||||
{
|
||||
Passphrase = dialog.Passphrase,
|
||||
Overwrite = dialog.Overwrite
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Erst ohne zu schreiben prüfen — Prüfsummen und Konflikte.
|
||||
var preview = await Task.Run(() =>
|
||||
_service.RestoreAsync(zipPath, dialog.TargetDirectory,
|
||||
options with { DryRun = true, Overwrite = true }));
|
||||
|
||||
if (preview.WouldOverwrite.Count > 0 && !dialog.Overwrite)
|
||||
{
|
||||
var answer = MessageBox.Show(this,
|
||||
$"Im Zielordner liegen bereits {preview.WouldOverwrite.Count} der enthaltenen " +
|
||||
"Dateien.\n\nVorhandene Dateien überschreiben?",
|
||||
"Wiederherstellen", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
|
||||
|
||||
if (answer == DialogResult.Cancel)
|
||||
return;
|
||||
|
||||
options = options with { Overwrite = answer == DialogResult.Yes };
|
||||
}
|
||||
|
||||
var result = await Task.Run(() =>
|
||||
_service.RestoreAsync(zipPath, dialog.TargetDirectory, options));
|
||||
|
||||
_logger?.LogInformation("Wiederherstellung nach {Target}: {Written} Dateien, {Skipped} übersprungen",
|
||||
dialog.TargetDirectory, result.Written.Count, result.Skipped.Count);
|
||||
|
||||
var summary = $"{result.Written.Count} Datei(en) wiederhergestellt nach:\n{dialog.TargetDirectory}";
|
||||
if (result.Skipped.Count > 0)
|
||||
summary += $"\n\n{result.Skipped.Count} vorhandene Datei(en) blieben unangetastet.";
|
||||
|
||||
if (!manifest.HasSecrets && manifest.SecretCount > 0)
|
||||
summary += "\n\nHinweis: Diese Sicherung enthält keine Zugangsdaten. " +
|
||||
"API-Schlüssel und Passwörter müssen neu eingetragen werden.";
|
||||
|
||||
summary += "\n\nDie wiederhergestellte Instanz wird erst nach einem Neustart " +
|
||||
"der Anwendung sichtbar.";
|
||||
|
||||
MessageBox.Show(this, summary, "Wiederherstellen",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Wiederherstellung fehlgeschlagen");
|
||||
MessageBox.Show(this, $"Die Wiederherstellung ist fehlgeschlagen:\n\n{ex.Message}",
|
||||
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schlägt einen neuen Ordner neben der Instanz vor, nicht die laufende Instanz
|
||||
/// selbst — diese hält Dateien geöffnet und würde Teile sofort überschreiben.
|
||||
/// </summary>
|
||||
private string SuggestRestoreTarget(BackupManifest manifest)
|
||||
{
|
||||
var parent = Directory.GetParent(_instanceDir)?.FullName ?? _instanceDir;
|
||||
var name = string.IsNullOrWhiteSpace(manifest.InstanceName)
|
||||
? "Instanz"
|
||||
: manifest.InstanceName;
|
||||
|
||||
var safeName = string.Concat(
|
||||
name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
|
||||
return Path.Combine(parent, $"Instance-{safeName}_wiederhergestellt_{DateTime.Now:yyyyMMdd_HHmm}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Rotation
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
/// <summary>Entfernt die ältesten Sicherungen dieser Instanz über der Aufbewahrungszahl.</summary>
|
||||
private void ApplyRotation(string folder)
|
||||
{
|
||||
var keep = (int)num_keepCount.Value;
|
||||
if (keep <= 0 || !Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var prefix = $"backup_{_instanceName}_";
|
||||
|
||||
var obsolete = new DirectoryInfo(folder)
|
||||
.GetFiles("*.zip")
|
||||
.Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(f => f.LastWriteTime)
|
||||
.Skip(keep)
|
||||
.ToList();
|
||||
|
||||
foreach (var file in obsolete)
|
||||
{
|
||||
file.Delete();
|
||||
_logger?.LogInformation("Alte Sicherung entfernt: {Name}", file.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Aufräumen darf eine erfolgreiche Sicherung nicht nachträglich entwerten.
|
||||
_logger?.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Kleinkram
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void SetStatus(string text, Color color)
|
||||
{
|
||||
label_createStatus.Text = text;
|
||||
label_createStatus.ForeColor = color;
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes) => bytes switch
|
||||
{
|
||||
< 1024 => $"{bytes} B",
|
||||
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
|
||||
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
|
||||
_ => $"{bytes / 1024.0 / 1024 / 1024:F2} GB"
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ClawdDotNet.UI;
|
||||
|
||||
public sealed record BridgeMessage(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("agentId")] string? AgentId = null,
|
||||
[property: JsonPropertyName("content")] string? Content = null,
|
||||
[property: JsonPropertyName("status")] string? Status = null,
|
||||
[property: JsonPropertyName("stepCount")] int? StepCount = null,
|
||||
[property: JsonPropertyName("tokenCount")] int? TokenCount = null,
|
||||
[property: JsonPropertyName("error")] string? Error = null,
|
||||
[property: JsonPropertyName("extra")] object? Extra = null
|
||||
);
|
||||
|
||||
public static class BridgeTypes
|
||||
{
|
||||
// C# → Browser
|
||||
public const string AgentListUpdate = "agent_list_update";
|
||||
public const string AgentStatusUpdate = "agent_status";
|
||||
public const string SelectAgent = "select_agent";
|
||||
public const string ChatMessage = "chat_message";
|
||||
public const string ChatTyping = "chat_typing";
|
||||
public const string ChatHistory = "chat_history";
|
||||
public const string RunStarted = "run_started";
|
||||
public const string RunFinished = "run_finished";
|
||||
|
||||
// Browser → C#
|
||||
public const string UserMessage = "user_message";
|
||||
public const string OpenAgentChat = "open_agent_chat";
|
||||
public const string RunNow = "run_now";
|
||||
public const string AbortRun = "abort_run";
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace ClawdDotNet.UI;
|
||||
|
||||
public static class EmbeddedUiManager
|
||||
{
|
||||
private static string? _extractedPath;
|
||||
|
||||
public static string ExtractToTemp()
|
||||
{
|
||||
if (_extractedPath is not null) return _extractedPath;
|
||||
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "ClawdDotNet_UI",
|
||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "dev");
|
||||
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
var asm = Assembly.GetExecutingAssembly();
|
||||
var prefix = "ClawdDotNet.EmbeddedUI.";
|
||||
|
||||
foreach (var name in asm.GetManifestResourceNames()
|
||||
.Where(n => n.StartsWith(prefix)))
|
||||
{
|
||||
// "ClawdDotNet.EmbeddedUI.chat.html" → "chat.html"
|
||||
var fileName = name[prefix.Length..];
|
||||
var dest = Path.Combine(tempDir, fileName);
|
||||
|
||||
using var stream = asm.GetManifestResourceStream(name)!;
|
||||
using var file = File.Create(dest);
|
||||
stream.CopyTo(file);
|
||||
}
|
||||
|
||||
_extractedPath = tempDir;
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
public static string GetExtractedPath()
|
||||
=> _extractedPath ?? throw new InvalidOperationException(
|
||||
"EmbeddedUiManager.ExtractToTemp() must be called first.");
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace ClawdDotNet.UI;
|
||||
|
||||
public sealed class WebViewBridge : IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly Microsoft.Web.WebView2.WinForms.WebView2 _wv;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public event Action<BridgeMessage>? MessageReceived;
|
||||
|
||||
public WebViewBridge(
|
||||
Microsoft.Web.WebView2.WinForms.WebView2 webView,
|
||||
ILogger logger)
|
||||
{
|
||||
_wv = webView;
|
||||
_logger = logger;
|
||||
_wv.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
|
||||
}
|
||||
|
||||
public async Task SendAsync(BridgeMessage message)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(message, JsonOpts);
|
||||
var script = $"window.__bridge?.receive({json})";
|
||||
|
||||
if (_wv.InvokeRequired)
|
||||
{
|
||||
await Task.Factory.FromAsync(
|
||||
_wv.BeginInvoke(new Func<Task>(async () =>
|
||||
await _wv.CoreWebView2.ExecuteScriptAsync(script))),
|
||||
_ => { });
|
||||
}
|
||||
else
|
||||
{
|
||||
await _wv.CoreWebView2.ExecuteScriptAsync(script);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = e.TryGetWebMessageAsString();
|
||||
_logger.LogDebug("Bridge raw incoming: {Raw}", raw);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(raw)) return;
|
||||
|
||||
var msg = JsonSerializer.Deserialize<BridgeMessage>(raw, JsonOpts);
|
||||
_logger.LogInformation("Bridge received: type={Type}, agentId={AgentId}, content={Content}",
|
||||
msg?.Type, msg?.AgentId, msg?.Content?.Length > 50 ? msg.Content[..50] + "..." : msg?.Content);
|
||||
|
||||
if (msg is not null)
|
||||
MessageReceived?.Invoke(msg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Bridge: failed to deserialize incoming message");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_wv.CoreWebView2 is not null)
|
||||
_wv.CoreWebView2.WebMessageReceived -= OnWebMessageReceived;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"instanceId": "crypto-01",
|
||||
"instanceName": "Krypto-Team",
|
||||
"openRouterApiKey": "sk-or-DEIN-API-KEY-HIER",
|
||||
"workingDirectory": "./data/crypto/",
|
||||
"logDirectory": "./Logs",
|
||||
"webServerPort": 8082,
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "crypto-analyst",
|
||||
"displayName": "Krypto-Analyst",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"systemPrompt": "Du analysierst Kryptowährungsmärkte und erstellst Trading-Signale.",
|
||||
"tools": {
|
||||
"Database": {
|
||||
"connectionString": "Server=localhost;Database=crypto;User=agent_crypto;Password=changeme;",
|
||||
"type": "mysql",
|
||||
"allowedTables": ["prices", "signals", "portfolio"]
|
||||
},
|
||||
"FileRW": {
|
||||
"rootPath": "./data/crypto/reports/",
|
||||
"allowWrite": true,
|
||||
"allowedExtensions": [".json", ".txt", ".md"]
|
||||
},
|
||||
"Mail": {
|
||||
"imapHost": "imap.example.com",
|
||||
"imapPort": 993,
|
||||
"smtpHost": "smtp.example.com",
|
||||
"smtpPort": 587,
|
||||
"username": "crypto-alerts@example.com",
|
||||
"password": "changeme",
|
||||
"allowedRecipients": ["owner@example.com"]
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"cron": "*/30 * * * *",
|
||||
"runOnStart": true
|
||||
},
|
||||
"loopGuard": {
|
||||
"maxSteps": 30,
|
||||
"maxTokens": 120000,
|
||||
"timeoutSeconds": 900
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"instanceId": "stock-01",
|
||||
"instanceName": "Aktien-Team",
|
||||
"openRouterApiKey": "sk-or-DEIN-API-KEY-HIER",
|
||||
"workingDirectory": "./data/stock/",
|
||||
"logDirectory": "./Logs",
|
||||
"webServerPort": 8081,
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "market-analyst",
|
||||
"displayName": "Marktanalyse",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"systemPrompt": "Du bist ein erfahrener Marktanalyst. Analysiere Marktdaten und erstelle Berichte.",
|
||||
"tools": {
|
||||
"Database": {
|
||||
"connectionString": "Server=localhost;Database=stocks;User=agent_analyst;Password=changeme;",
|
||||
"type": "mysql",
|
||||
"allowedTables": ["quotes", "indicators", "news"]
|
||||
},
|
||||
"FileRW": {
|
||||
"rootPath": "./data/stock/analyst/",
|
||||
"allowWrite": true,
|
||||
"allowedExtensions": [".json", ".txt", ".md"]
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"cron": "0 7 * * 1-5",
|
||||
"runOnStart": false
|
||||
},
|
||||
"loopGuard": {
|
||||
"maxSteps": 25,
|
||||
"maxTokens": 100000,
|
||||
"timeoutSeconds": 600
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "webdev",
|
||||
"displayName": "Web-Entwickler",
|
||||
"model": "google/gemini-flash-1.5",
|
||||
"systemPrompt": "Du erstellst HTML-Dashboards aus bereitgestellten Daten.",
|
||||
"tools": {
|
||||
"FileRW": {
|
||||
"rootPath": "./data/stock/wwwroot/",
|
||||
"allowWrite": true,
|
||||
"allowedExtensions": [".html", ".css", ".js", ".json"]
|
||||
}
|
||||
},
|
||||
"loopGuard": {
|
||||
"maxSteps": 10,
|
||||
"maxTokens": 40000,
|
||||
"timeoutSeconds": 300
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
# Agentenkommunikation — Erfassung, Ansicht, Auswertung
|
||||
|
||||
> **Bauplan — noch nicht gebaut.** Die fünf Phasen sind in der [Roadmap](Roadmap.md) 3.3
|
||||
> einzeln eingeordnet; die Tiefenbegrenzung (B8) ist dort Punkt 4 der Reihenfolge.
|
||||
> Die Ansicht aus Phase 4 steht bereits und wartet auf den Speicher aus Phase 2.
|
||||
|
||||
Ziel: Die **gesamte** Kommunikation zwischen Agenten wird erfasst, ist im WhatsApp-Stil
|
||||
paarweise nachlesbar und lässt sich von einem Agenten automatisiert auswerten — um zu
|
||||
finden, wo die Zusammenarbeit klemmt.
|
||||
@@ -222,9 +226,11 @@ zu sehen, mit wem er überhaupt spricht. Zeitraum, Kanalfilter, Freitextsuche.
|
||||
trägt eine kleine Kennzeichnung des Kanals (`Rückfrage` / `Auftrag` / `Kommentar` /
|
||||
`Ergebnis`) und, wo vorhanden, die anklickbare Task-Id.
|
||||
|
||||
**Technisch**: über den vorhandenen **WebView2**-Unterbau, wie ihn `frm_chat` schon nutzt
|
||||
**Technisch**: damals über den WebView2-Unterbau von `frm_chat` gedacht. Beides gibt es
|
||||
seit der Avalonia-Portierung nicht mehr — der Abschnitt ist als Entwurfsstand von damals
|
||||
zu lesen; die Ansicht wäre heute eine Avalonia-Seite wie `AgentChatsPageView`
|
||||
(inkl. Virtual-Host-Mapping auf einen lokalen Ordner). Der Verlauf wird als HTML
|
||||
gerendert. Handgezeichnete Sprechblasen in WinForms wären ein Vielfaches an Aufwand für
|
||||
gerendert. Handgezeichnete Sprechblasen wären dort ein Vielfaches an Aufwand für
|
||||
ein schlechteres Ergebnis.
|
||||
|
||||
**Paging**: die jüngsten ~200 Nachrichten, „ältere laden" nach oben. Ein Paar mit 50.000
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Audit-Log & Receipts — Nachvollziehbarkeit
|
||||
|
||||
> **Bauplan zu einem gebauten System.** Umgesetzt; der Stand steht in der
|
||||
> [Roadmap](Roadmap.md) 3.1.
|
||||
|
||||
Setzt A3 aus der [Roadmap](Roadmap.md) um (F-A2). Zwei zusammengehörige Dinge:
|
||||
|
||||
- **Audit-Log** — ein Eintrag je Tool-Aufruf: wer, wann, welches Tool, mit welchem
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Deploymentcenter-Integration
|
||||
|
||||
> **Beschreibung der Verdrahtung** (Heartbeat, Fehler-Stream, Bugtracker, Updates).
|
||||
> Offene Punkte dazu stehen in der [Roadmap](Roadmap.md) 3.5, nicht hier.
|
||||
|
||||
Stand: 2026-08-08, Deploymentcenter **2.1**. Ersetzt den früheren
|
||||
`Integrationsplan-WatchDog-LicenseLabrador.md`.
|
||||
|
||||
@@ -179,7 +182,7 @@ Die frühere Fassung führte einen Ed25519-Public-Key als „Vertrauensanker". I
|
||||
Deploymentcenter gibt es dazu keine Gegenseite — der Client liest ausschließlich das Feld
|
||||
`status`. Ein Schlüssel, der nichts prüft, ist schlimmer als keiner: Er lässt Schutz
|
||||
vermuten, wo keiner ist. Details in
|
||||
[Deploymentcenter-Anbindung-Review](Deploymentcenter-Anbindung-Review.md), Abschnitt 2.1.
|
||||
[Deploymentcenter-Anbindung-Review](archiv/Deploymentcenter-Anbindung-Review.md), Abschnitt 2.1.
|
||||
|
||||
### Der Projekt-Slug ist `clawddotnet`
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Memory — Langzeitgedächtnis für Agenten
|
||||
|
||||
Löst K1 aus der [Bestandsaufnahme](Bestandsaufnahme-2026-07.md): Geplante Agenten
|
||||
> **Bauplan zu einem gebauten System.** Umgesetzt; der Stand steht in der
|
||||
> [Roadmap](Roadmap.md) 3.2. Die drei Punkte unter „Offen" laufen dort weiter.
|
||||
|
||||
Löst K1 aus der [Bestandsaufnahme](archiv/Bestandsaufnahme-2026-07.md): Geplante Agenten
|
||||
begannen bei jedem Cron-Lauf bei null. Ein Agent, der alle 30 Minuten lief, wusste
|
||||
nichts von seinem letzten Durchgang — er rief dieselben Quellen ab, zog dieselben
|
||||
Schlüsse und konnte keine Entwicklung über Zeit verfolgen.
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
# Avalonia-Portierung — Leitfaden
|
||||
# Oberfläche — Leitfaden
|
||||
|
||||
Für alle, die weitere Ansichten von WinForms nach Avalonia übertragen.
|
||||
Stand: 2026-08-07.
|
||||
Für alle, die an `src/ClawdDotNet.Desktop` arbeiten.
|
||||
Stand: 2026-08-23.
|
||||
|
||||
---
|
||||
|
||||
## 1. Auftrag
|
||||
## 1. Stand
|
||||
|
||||
Drei Bereiche des Hauptfensters sind noch Platzhalter. In dieser Reihenfolge portieren —
|
||||
sie steigen im Umfang, und jede baut auf dem Muster der vorigen auf:
|
||||
**Die Portierung ist abgeschlossen.** Es gibt keine Platzhalter mehr und keine
|
||||
WinForms-Vorlage, gegen die man vergleichen könnte: Die alte Oberfläche
|
||||
(`ClawdDotNet.csproj`, `frm_*.cs`, `UI/`, `Models/`, `EmbeddedUI/`) ist am
|
||||
2026-08-23 aus dem Arbeitsbaum entfernt worden. Wer sie doch einmal braucht,
|
||||
findet sie im Tag `vor-fruehjahrsputz-2026-08`.
|
||||
|
||||
| # | Bereich | WinForms-Vorlage | Daten aus |
|
||||
|---|---|---|---|
|
||||
| 1 | **Info** | `frm_main.Designer.cs`, Suchwort `tabPage_Info` | `AppHost.AppVersion`, `AppHost.BuildSummary`, `host.Instance` |
|
||||
| 2 | **Sicherung** | `UI/BackupPanel.cs` + `UI/BackupPanel.Designer.cs` | `host.InstancePath`, `host.Settings`, `Core.Backup.BackupService` |
|
||||
| 3 | **Aufgaben** (Jobs/Services/Verlauf) | `frm_main.cs`, Abschnitt `WORKER TAB` ab Zeile 932 | `host.Instance.Agents`, `App.Services.JobHistoryService` |
|
||||
Neun Bereiche, alle nativ in Avalonia:
|
||||
|
||||
**Nicht anfassen:** Chat und Einstellungen. Beide sind Entwurfsarbeit, nicht Übersetzung,
|
||||
und werden gesondert gemacht.
|
||||
| Gruppe | Bereich | Ansichtsmodell |
|
||||
|---|---|---|
|
||||
| Arbeit | Chat, Agenten, Aufgaben | `ChatPageViewModel`, `AgentsPageViewModel`, `TasksPageViewModel` |
|
||||
| Analyse | Token-Verbrauch, Agenten-Chats | `TokenUsagePageViewModel`, `AgentChatsPageViewModel` |
|
||||
| System | Sicherung, Protokoll, Einstellungen, Info | `BackupPageViewModel`, `LogPageViewModel`, `SettingsPageViewModel`, `InfoPageViewModel` |
|
||||
|
||||
Die WinForms-Dateien liegen noch im Repository, sind aber **nicht mehr Teil des Builds**
|
||||
(siehe Kommentar in `ClawdDotNet.slnx`). Sie sind Vorlage zum Lesen — nicht zum Kompilieren,
|
||||
nicht zum Reparieren.
|
||||
Dazu die Fenster `InstancePickerWindow`, `LicenseWindow`, `TextEditorWindow` und
|
||||
die Dialoge zum Anlegen von Agent, Auftrag und Dienst.
|
||||
|
||||
Das Erscheinungsbild folgt dem Entwurf in `Mockup/` — wer daran etwas ändert,
|
||||
liest zuerst `Mockup/extracted/mockup/Implementierungsleitfaden.md`.
|
||||
|
||||
**Was in der Oberfläche noch fehlt**, siehe [Roadmap](Roadmap.md):
|
||||
die Freigabe-Ansicht für gestagte Aufrufe (A2) und die Schaltfläche
|
||||
„Fehler melden" (DC1).
|
||||
|
||||
---
|
||||
|
||||
@@ -1,308 +1,345 @@
|
||||
# Roadmap
|
||||
# ClawdDotNet — Roadmap
|
||||
|
||||
Zentrale Liste aller offenen Vorhaben. Sie löst die beiden „Vorgeschlagene
|
||||
Reihenfolge"-Abschnitte in der [Bestandsaufnahme](Bestandsaufnahme-2026-07.md) und im
|
||||
[Konzepte-Dokument](Konzepte-Backup-Finanz-Analyse.md) ab — die bleiben als Befund bzw.
|
||||
Konzept bestehen, gepflegt wird nur noch hier.
|
||||
**Dies ist die einzige Vorhabenliste.** Vor dem 2026-08-23 lag der Stand über eine
|
||||
Bestandsaufnahme, drei Konzeptpapiere, vier Umsetzungspläne und zwei
|
||||
Deploymentcenter-Dokumente verteilt — jedes mit eigener Reihenfolge, teils
|
||||
widersprüchlich. Alles davon ist hier zusammengeführt.
|
||||
|
||||
Kürzel (S4, K2, T4, F-A1, …) verweisen auf die Bestandsaufnahme.
|
||||
Gepflegt wird nur noch dieses Dokument. Was aus den alten Papieren wurde, steht in
|
||||
[Abschnitt 7](#7--woher-das-hier-kommt).
|
||||
|
||||
---
|
||||
|
||||
## A — Beschlossen (aus dem OpenAlice-Vergleich, Juli 2026)
|
||||
## Wie das hier zu lesen ist
|
||||
|
||||
Hintergrund: Konzeptvergleich mit [OpenAlice](https://github.com/TraderAlice/OpenAlice)
|
||||
(AGPL-3.0 — Konzepte übernehmen ja, Code nein). Übernommen werden Taskboard,
|
||||
Staging-Freigabe, Audit-Log und das Skill-Modell. Die Inbox-Idee entfällt zugunsten
|
||||
der geplanten Matrix-Migration (A5).
|
||||
|
||||
### A1 — Taskboard
|
||||
|
||||
Aufgaben als Markdown-Dateien mit YAML-Frontmatter im `SharedWorkspace`:
|
||||
`title`, `status` (`backlog | todo | in_progress | done | canceled`), `priority`,
|
||||
`assignee`, optional `when` (`at` | `every` | `cron` **mit Zeitzone**).
|
||||
|
||||
- **Scanner statt Delay-Schleifen**: Ein Takt (~60 s) prüft, was fällig ist.
|
||||
Persistiert werden nur Last-Fired-Marker — ein fehlgeschlagener Lauf bleibt der
|
||||
einzige Versuch für diesen Termin, kein automatischer Retry-Sturm.
|
||||
- **Assignee bestimmt die Ausführung**: `@new` = frischer Lauf ohne Historie,
|
||||
`@<agent>` = bestehender Agent mit seinem Kontext, `@human` = wartet auf uns.
|
||||
Das ersetzt das implizite `UseChatContext`-Flag (T7) durch eine explizite Angabe
|
||||
am Auftrag.
|
||||
- **Agenten-Tool**: `task_create`, `task_list`, `task_update`, `task_comment`.
|
||||
Agent-zu-Agent-Delegation läuft künftig über Tasks statt über rekursives
|
||||
`send_message`.
|
||||
- **Migration**: Die improvisierten `coordination/*.md`-Dateien der Agenten
|
||||
(task_*, status_*, broadcast) gehen im Taskboard auf.
|
||||
|
||||
**Detailbauplan** (aus dem Fünf-Repo-Vergleich, Juli 2026 beschlossen):
|
||||
|
||||
- Status zusätzlich mit **`in_review`**; im Frontmatter **`require_approval`**
|
||||
(Task gilt erst nach Review als done) und **`acceptance`** (Abnahmekriterien,
|
||||
gegen die das Ergebnis geprüft wird).
|
||||
- **Task-Typen `approval` und `human_input`** — ein Mensch ist einfach ein
|
||||
Assignee; seine Antwort ist das Task-Ergebnis und Input für Folgetasks.
|
||||
- **Atomares Claiming**: Die DB verhindert, dass zwei Läufe denselben Task
|
||||
ziehen. Der Scanner arbeitet mit **Claim-before-run** (at-most-once — ein
|
||||
doppelter Tick findet den Claim bereits vergeben) und
|
||||
**Startup-Reconciliation**: Beim Start wird Soll (Frontmatter) gegen Ist
|
||||
(Marker/Claims) abgeglichen, verpasste Läufe werden erkannt statt still
|
||||
übersprungen.
|
||||
- **`blocked_by`-Abhängigkeiten** mit Auto-Dispatch: Wird der letzte Blocker
|
||||
fertig, wird der wartende Task automatisch angestoßen. Meldet ein Agent einen
|
||||
Blocker, fällt der Task und der Zuständige (Lead/Benutzer) wird benachrichtigt
|
||||
(**Blocker-Eskalation**).
|
||||
- **Reopen-/Feedback-Semantik**: Ergebnis + Kritik gehen per `task_comment` an
|
||||
denselben Agenten zur Nachbesserung zurück, statt einen neuen Task von vorn
|
||||
zu beginnen.
|
||||
|
||||
Damit erledigt oder aufgegangen:
|
||||
|
||||
| Punkt | Warum |
|
||||
| Zeichen | Bedeutung |
|
||||
|---|---|
|
||||
| F-A5 Task-Queue | das Taskboard **ist** die Queue |
|
||||
| F-A4 Run-Historie | Läufe werden am Task verknüpft und persistiert |
|
||||
| B8 Rekursion `send_message` | Delegation über Tasks ist strukturell zyklenfrei |
|
||||
| B6 `Task.Delay`-Überlauf | Scanner-Modell kennt keine langen Delays |
|
||||
| B7 Cron in Lokalzeit | Frontmatter-`when` ist zeitzonen-explizit |
|
||||
| T7 `RunAsync` vs. `ChatAsync` | Assignee-Semantik beantwortet die Frage |
|
||||
| ✅ | erledigt |
|
||||
| 📋 | **beschlossen**, noch nicht gebaut — kann jederzeit angefasst werden |
|
||||
| ❓ | **Entscheidung nötig**, bevor gebaut werden kann |
|
||||
| ❄️ | zurückgestellt — wartet auf eine Entscheidung oder ein anderes Vorhaben |
|
||||
| 💭 | **Idee**, nicht beschlossen — bewusst geparkt, damit sie nicht verlorengeht |
|
||||
|
||||
Verzahnung: Das Marktkalender-Flag (`onlyWhenMarketOpen`, siehe C1) gehört ins
|
||||
Frontmatter, nicht in einen eigenen Mechanismus.
|
||||
Die **Herkunft**-Spalte trägt das alte Kürzel (S4, K2, B9, T4, F-A1, C1, DC1 …). Damit
|
||||
bleiben die archivierten Papiere auffindbar, ohne dass man sie lesen muss.
|
||||
|
||||
Konzept-Doc: [Taskboard-Konzept](Taskboard-Konzept.md) (Dateiformat,
|
||||
Wahrheitsaufteilung Datei/DB, Scanner-Verhalten, Invarianten, Migration).
|
||||
|
||||
### A2 — Staging-Freigabe für irreversible Aktionen (F-A1 + S4)
|
||||
|
||||
Konzept-Doc: [Staging-Konzept](Staging-Konzept.md).
|
||||
|
||||
Irreversible Aktionen (Mail senden, X posten, DB-Schreibzugriff, Datei löschen,
|
||||
perspektivisch Orders) werden **gestaged statt ausgeführt**: Vorschlag → Review im
|
||||
Hauptfenster → Freigabe/Ablehnung. Pro Tool/Aktion konfigurierbar:
|
||||
`auto | approve | deny`.
|
||||
|
||||
Das bisher wirkungslose `PermissionGate` (S4) wird dabei zum zentralen
|
||||
Durchsetzungspunkt ausgebaut: Policy-Prüfung, Staging-Entscheidung und Audit-Hook
|
||||
(A3) an einer Stelle statt ad-hoc in jedem Tool. S4 wird nicht separat bearbeitet,
|
||||
sondern geht hier auf.
|
||||
|
||||
Ergänzungen (Juli 2026 beschlossen):
|
||||
|
||||
- **Plan-Freeze**: Freigegeben wird ein eingefrorener, konkreter Aufruf — Tool,
|
||||
Aktion und exakte Argumente zum Zeitpunkt des Stagings. Ausgeführt wird genau
|
||||
das Eingefrorene; jede nachträgliche Änderung ist eine neue Freigabe.
|
||||
- **Approval-Records**: Jede Entscheidung (Freigabe wie Ablehnung) wird als
|
||||
Datensatz im Audit-Log (A3) verankert — wer, wann, was, mit welchem Ergebnis.
|
||||
|
||||
Sicherheitswirkung: Eine Prompt-Injection (K2) kann dann nur noch einen Vorschlag
|
||||
erzeugen, keine Ausführung.
|
||||
|
||||
### A3 — Audit-Log (F-A2)
|
||||
|
||||
Konzept-Doc: [Audit-Konzept](Audit-Konzept.md).
|
||||
|
||||
Jeder Tool-Aufruf wird protokolliert: Agent, Lauf/Session, Zeitstempel, Argumente,
|
||||
Ergebnis-Status. Append-only (JSONL oder SQLite-Tabelle auf dem vorhandenen
|
||||
`SqliteStorage`).
|
||||
|
||||
Designregeln (aus dem OpenAlice-Provenance-Konzept):
|
||||
|
||||
- Herkunft wird **von der Engine gestempelt**, nie vom Agenten behauptet.
|
||||
- Einträge sind unveränderlich; Korrekturen sind neue Einträge.
|
||||
- Unbekannte Herkunft wird als unbekannt markiert, nicht geraten.
|
||||
- Worker-Typ (Modell/Engine) und verantwortliche Session sind getrennte Begriffe.
|
||||
|
||||
Ergänzung (Juli 2026 beschlossen) — **Receipts**: Jeder abgeschlossene Task/Lauf
|
||||
erhält einen Abschluss-Beleg mit Ergebnis-Verweis, Schritten, Tokens und Kosten
|
||||
(Verknüpfung `RunUsage` ↔ Task). Damit fällt C7 („Kosten pro Ergebnis")
|
||||
weitgehend als Abfallprodukt ab.
|
||||
|
||||
Das Audit-Log ist zugleich das Fundament für das Ergebnisregister (C7) und die
|
||||
Tool-Fehlerquote aus der Leistungsanalyse.
|
||||
|
||||
### A4 — Skill-/Toolset-Modell (ersetzt T6)
|
||||
|
||||
Dreischichtig statt „alles immer im System-Prompt":
|
||||
|
||||
1. **Dauerhafter Kern** — Identity, Soul, unveränderliche Regeln. Schlank, damit der
|
||||
Prompt-Cache (T1) stabil bleibt.
|
||||
2. **Nachladbare Skills/Toolsets** — fachliche Abläufe und selten genutzte Tools
|
||||
werden erst auf Anforderung geladen (`list_toolsets` → `load_toolset`).
|
||||
3. **Selbstkorrigierende Tool-Fehler** — Fehlermeldungen nennen die gültigen
|
||||
Parameter/Aktionen, statt das Modell raten zu lassen.
|
||||
|
||||
Mechanik (Juli 2026 beschlossen, nach GoClaw-Vorbild): Skills liegen als
|
||||
`SKILL.md` mit Frontmatter (`name`, `description`) im Instanz- bzw.
|
||||
Agenten-Verzeichnis. Bei wenigen Skills werden die Kurzbeschreibungen inline in
|
||||
den Prompt eingebettet, bei vielen gibt es stattdessen ein `skill_search`-Tool.
|
||||
Änderungen an Skill-Dateien werden per Hot-Reload übernommen.
|
||||
|
||||
Dazu Hermes' Selbstverbesserungs-Idee: **Agenten dürfen Skills aus Erfahrung
|
||||
selbst schreiben** (das AgentEditor-Tool ist die Vorstufe). Wichtig: Ein Skill
|
||||
ist Prompt-Input — agentengeschriebene Skills werden erst nach Freigabe (A2)
|
||||
aktiv, sonst wäre das ein Injection-Kanal in künftige Läufe.
|
||||
|
||||
### A5 — Matrix/Element-Migration
|
||||
|
||||
Beschlossene Richtung: Die Kommunikation (Benachrichtigungen, Berichte, Chat mit
|
||||
Agenten) wird auf Element/Matrix umgestellt.
|
||||
|
||||
- Ersetzt die im OpenAlice-Vergleich erwogene Inbox — Berichte landen in
|
||||
Matrix-Räumen.
|
||||
- Der Tool-Kandidat **Notify** entfällt und geht hierin auf.
|
||||
- Betroffen: Telegram-Tool (Rolle klären), WebView-Chat (bleibt er Haupt-UI?),
|
||||
Streaming K4 (Matrix streamt nicht — nach dieser Entscheidung neu bewerten).
|
||||
|
||||
Scope ist noch unbestimmt — braucht ein eigenes Konzept-Doc, bevor es in die
|
||||
Reihenfolge eingeordnet wird.
|
||||
|
||||
### A6 — MySQL-Replikations-Spiegel (optional)
|
||||
|
||||
Beschlossen Juli 2026. **SQLite bleibt die einzige Wahrheit** — gearbeitet wird
|
||||
ausschließlich auf der Instanz-DB. MySQL ist ein reiner, nachgelagerter Spiegel:
|
||||
Er empfängt nur INSERT/UPDATE/DELETE vom Replikator; die App liest im Betrieb
|
||||
**nie** daraus. Einziger Lesezweck: Wiederherstellung, falls die SQLite korrupt
|
||||
ist — daneben steht der Spiegel externen Auswertungen (Dashboards, Ad-hoc-SQL)
|
||||
offen, ohne die Agenten-Maschine zu berühren.
|
||||
|
||||
Leitplanken:
|
||||
|
||||
- **Outbox-Muster** an der vorhandenen Schreib-Warteschlange von `SqliteStorage`:
|
||||
Replikations-Einträge lokal puffern, idempotente Upserts nach MySQL.
|
||||
Nie blockierend — ist MySQL nicht erreichbar, staut die Outbox und holt auf.
|
||||
- Mehrere Instanzen replizieren in denselben Spiegel; Zeilen tragen `instance_id`.
|
||||
- **Restore-Pfad (Spiegel → frische SQLite) muss existieren und getestet sein** —
|
||||
gleiche Regel wie beim Backup: ein ungeprüftes Restore ist eine Vermutung.
|
||||
- Kein Koordinationspunkt: Claiming, Locks, Taskboard-Zustand bleiben lokal.
|
||||
Die Regel „nur der Replikator schreibt, niemand liest im Betrieb" gehört ins
|
||||
Konzept-Doc.
|
||||
- Sicherheit: TLS zur Datenbank, Zugangsdaten über `SecretProtector`;
|
||||
Output-Scrubbing der Tool-Ergebnisse wird wichtiger, weil Kontext-Daten
|
||||
künftig auch im Spiegel liegen.
|
||||
- Ergänzt das ZIP-Backup, ersetzt es nicht — Identity/Soul, Settings, Workspace
|
||||
und Telegram-Session bleiben Sache des Instanz-Backups.
|
||||
|
||||
**Voraussetzung — Historie-Umzug:** `ChatHistory.json` zieht in die Instanz-DB um
|
||||
(aktive Tabelle + Archiv-Tabelle mit FTS5-Volltextindex und `history_search`-Tool,
|
||||
siehe K6 in Abschnitt B). Das löst
|
||||
nebenbei B10 (O(n²)-Schreiblast) und gibt K6 seine Form; erst danach schützt der
|
||||
Spiegel auch die Historie. Zeitlich passt A6 zu dem Server, der ggf. mit A5
|
||||
(Matrix) ohnehin dazukommt.
|
||||
**Bauplan** heißt: Es gibt ein Dokument, das die Umsetzung im Detail beschreibt. Wer den
|
||||
Punkt anfasst, liest es zuerst.
|
||||
|
||||
---
|
||||
|
||||
## B — Offen aus der Bestandsaufnahme
|
||||
## 0 — Stand
|
||||
|
||||
| Punkt | Was | Stand |
|
||||
|---|---|---|
|
||||
| T4 | Proaktiv statt reaktiv kompaktieren | offen, unverändert |
|
||||
| K2-Rest | Untrusted Content als Daten rahmen (`<untrusted_content>`) | A2 nimmt die Schärfe; die Rahmung selbst bleibt nötig |
|
||||
| K4 | Streaming | **zurückgestellt** bis A5 entschieden ist |
|
||||
| K6 | Historie **archivieren + durchsuchbar machen** (FTS5-Index, `history_search`-Tool) statt nur rotieren | beschlossen; Teil des Historie-Umzugs (Voraussetzung von A6) |
|
||||
| Memory-Flush vor Compaction | Bevor der ContextCompactor zusammenfasst, bekommt der Agent ein eng begrenztes Fenster, Dauerhaftes per `memory_store` zu sichern — sonst wirft die Compaction Wissen weg | beschlossen (GoClaw-Muster) |
|
||||
| Memory-Auto-Injection | Relevante Memory-Abstracts werden automatisch eingeblendet (Relevanzschwelle, Deckel ~200 Tokens), **in die Nutzernachricht, nie in den System-Prompt** (Prompt-Cache T1) | beschlossen; löst den offenen Punkt „Automatische Einblendung" im [Memory-Konzept](Memory-Konzept.md) |
|
||||
| Output-Scrubbing | Bekannte Secret-Werte (Register des `SecretProtector`) werden zentral aus **allen** Tool-Ergebnissen maskiert, bevor sie in Kontext, Historie oder Spiegel (A6) gelangen | beschlossen; schließt die Lücke, die S3 nur für URLs schloss |
|
||||
| Hygiene-Paket | B9 (`index_Count`-Race), B11/T8 (`max_tokens` setzen), B13 (`instanceId`-Inkonsistenz), F-A6-Rest (UI zum Setzen/Rotieren der Secrets) | Kleinbugs, in einem Aufwasch. B10 geht im Historie-Umzug (A6) auf |
|
||||
Der Kern trägt. Engine, Taskboard, Audit, Gedächtnis, Budget, Sicherung und die
|
||||
Oberfläche sind gebaut und getestet (585 Tests grün, Build fehlerfrei, keine bekannten
|
||||
Sicherheitslücken in den Paketen). Die Sicherheitsbefunde S1–S7 und die Bugs B1–B5,
|
||||
B11, B12 aus der Bestandsaufnahme sind abgearbeitet.
|
||||
|
||||
Erledigt seit der letzten Fortschreibung: B4 vollständig (Preise kommen live vom
|
||||
`/models`-Endpunkt, unbekannte Modelle werden sichtbar gemeldet).
|
||||
Drei Dinge stehen im Weg:
|
||||
|
||||
### B-DC — Deploymentcenter-Anbindung
|
||||
1. **Die Freigabe-Ansicht fehlt.** Staging ist gebaut und greift — aber niemand kann
|
||||
freigeben. Damit ist der unbeaufsichtigte Betrieb, für den das ganze A2-Vorhaben da
|
||||
war, nicht möglich.
|
||||
2. **A5 ist unentschieden.** Rocket.Chat ist gebaut und bedient denselben Zweck, für den
|
||||
Matrix vorgesehen war. Solange das offen ist, hängen Streaming (K4), der
|
||||
Notfallkanal und ein Teil von A6 in der Luft.
|
||||
3. **Agenten können sich gegenseitig verklemmen.** `AgentComm.send_message` hat keine
|
||||
Tiefenbegrenzung; A→B→A blockiert bis zum Timeout. Bekannt seit Juli, unverändert.
|
||||
|
||||
---
|
||||
|
||||
## 1 — Reihenfolge
|
||||
|
||||
Was als Nächstes ansteht, mit Begründung. Alles darunter in Abschnitt 3.
|
||||
|
||||
| # | Vorhaben | Warum an dieser Stelle | Bereich |
|
||||
|---|---|---|---|
|
||||
| **1** | **Freigabe-Ansicht** (Rest von A2) | Der einzige Punkt, der echten Betrieb blockiert. Die Dienst-API steht vollständig; es fehlt eine Seite mit Liste und zwei Schaltflächen | [3.1](#31--kontrolle-und-sicherheit) |
|
||||
| **2** | **A5 entscheiden**: Matrix oder Rocket.Chat | Kostet keine Umsetzung, löst aber drei blockierte Punkte auf einmal. Deshalb früh | [2](#2--offene-entscheidungen) |
|
||||
| **3** | **FileRW-Papierkorb** | Kleinster Eingriff mit realer Wirkung: `delete` ist heute endgültig, ein Aufruf mit `path: "."` räumt einen Workspace ab. Danach kann `FileRW.delete` von `approve` auf `auto` — das entlastet die Freigabe-Ansicht sofort | [3.2](#32--agenten-fähigkeiten) |
|
||||
| **4** | **Delegationstiefe begrenzen** (B8) | Behebt eine echte Verklemmung. Kleiner Eingriff, aber am Agent-Gate — mit Test A13 aus der [Teststrategie](Teststrategie.md) | [3.3](#33--kommunikation-zwischen-agenten) |
|
||||
| **5** | **Output-Scrubbing** | Muss **vor** allem stehen, was Tool-Ergebnisse dauerhaft speichert (Agentenkommunikation Phase 2, MySQL-Spiegel). Sonst legen wir einen Speicher an, der nachträglich bereinigt werden müsste | [3.1](#31--kontrolle-und-sicherheit) |
|
||||
| **6** | **C1 Marktkalender** | Spart ab dem ersten Tag Geld: Ein `*/30`-Cron läuft heute auch Sonntag um 3 Uhr. Der Haken im Scanner ist schon da (`IMarketCalendar`) | [3.6](#36--finanz-und-analyse) |
|
||||
| **7** | **A4 Skills/Toolsets** | Größter Token-Hebel nach dem Caching, und Voraussetzung dafür, dass Agenten viele Tools haben können, ohne dass jeder Schritt teuer wird | [3.2](#32--agenten-fähigkeiten) |
|
||||
| **8** | **Historie-Umzug in die Instanz-DB** | Löst B10 (O(n²)-Schreiblast) und K6 (unbegrenzte Historie) in einem Zug und ist Voraussetzung für A6 | [3.4](#34--daten-und-speicher) |
|
||||
| — | **DC1 „Fehler melden"** | Zwischendurch; der Client steht, es fehlt die Schaltfläche | [3.5](#35--betrieb-und-auslieferung) |
|
||||
| — | **Hygiene-Paket** | Zwischendurch, unabhängig von allem | [3.7](#37--hygiene) |
|
||||
|
||||
Leitlinie: **erst Bremsen, dann PS.** Ein Agent, der unbeaufsichtigt läuft, braucht
|
||||
Nachvollziehbarkeit und Kontrolle, bevor er mehr können soll. Deshalb stehen Freigabe,
|
||||
Papierkorb, Tiefenbegrenzung und Scrubbing vor den Fähigkeiten.
|
||||
|
||||
---
|
||||
|
||||
## 2 — Offene Entscheidungen
|
||||
|
||||
Diese Punkte kann niemand bauen, bevor sie entschieden sind. Sie kosten kein
|
||||
Entwicklungsbudget, nur eine Festlegung.
|
||||
|
||||
| # | Frage | Was daran hängt | Empfehlung aus dem Konzept |
|
||||
|---|---|---|---|
|
||||
| **E1** ❓ | **Matrix oder Rocket.Chat?** Rocket.Chat ist gebaut und bedient A5. Bleibt Matrix Ziel, oder entfällt A5? | K4 (Streaming), ob der `ChannelRouter` Pflicht oder Kür ist, wo A6 wohnt | Keine — bewusst offengelassen ([RocketChat-Konzept §4](RocketChat-Nextcloud-Konzept.md)) |
|
||||
| **E2** ❓ | **Nextcloud-Identität**: ein Dienstkonto mit Ordnern je Agent, oder je Agent ein eigener Benutzer? | Zuschnitt des Nextcloud-Tools | Dienstkonto |
|
||||
| **E3** ❓ | **Öffentliche Freigabe-Links** grundsätzlich erlauben (mit Freigabe) oder hart sperren? | Voreinstellung `allowPublicShares` | offen |
|
||||
| **E4** ❓ | **`ask_agent` behalten** (eng gefasst, Tiefe 1) oder ersatzlos streichen und alles über Tasks? | Zuschnitt von Phase 3 der Agentenkommunikation | behalten, eng gefasst |
|
||||
| **E5** ❓ | **Notfallkanal-Reihenfolge** instanzweit oder je Agent einstellbar? | Kleinigkeit am `ChannelRouter` | instanzweit |
|
||||
| **E6** ❓ | **`catchUp: true\|false`** je Task abschaltbar machen? | Verhalten des Scanners bei verpassten Terminen | offen |
|
||||
| **E7** ❓ | **Priorität bei mehreren fälligen Tasks**: strikt oder gewichtet? | Reihenfolge im Scanner | offen |
|
||||
| **E8** ❓ | **Aufbewahrungsdauer** der Agentenkommunikation | Vorgabewert im Speicher | unbegrenzt, bis A6 steht; dann 180 Tage lokal |
|
||||
| **E9** ❓ | **Automatische Orderausführung** — überhaupt? | Der gesamte Handels-Zweig. Broker-API, Teilausführungen, rechtlicher Rahmen — eigene Kategorie, kein Nebenprodukt der Analyse-Agenten | bewusst **nicht** vorgesehen |
|
||||
|
||||
---
|
||||
|
||||
## 3 — Beschlossen und offen
|
||||
|
||||
### 3.1 — Kontrolle und Sicherheit
|
||||
|
||||
Bauplan: [Staging-Konzept](Staging-Konzept.md), [Audit-Konzept](Audit-Konzept.md).
|
||||
|
||||
| Punkt | Herkunft | Stand | Was |
|
||||
|---|---|---|---|
|
||||
| Audit-Log + Receipts | F-A2, A3 | ✅ | Append-only auf SQLite. Herkunft wird von der Engine gestempelt, nie vom Agenten behauptet. Einträge unveränderlich, Korrekturen sind neue Einträge. Receipts verknüpfen Lauf ↔ Task mit Schritten, Tokens, Kosten |
|
||||
| Staging-Kern | F-A1, S4, A2 | ✅ | `PermissionGate` ist zum Durchsetzungspunkt ausgebaut. Policy `auto\|approve\|deny` je Tool/Aktion, Plan-Freeze (der eingefrorene Aufruf wird ausgeführt, nicht ein neu formulierter), atomarer Übergang `pending → approved`, Fortsetzung über Folge-Task statt pausiertem Lauf |
|
||||
| **Freigabe-Ansicht** | A2 | 📋 **#1** | Seite in `src/ClawdDotNet.Desktop`: offene Vorschläge listen, freigeben, ablehnen (mit Grund). `StagingService.ListPendingAsync/ApproveAsync/RejectAsync` steht bereit, `AppHost.Staging` reicht ihn durch. **Ohne sie liegt jeder gestagte Aufruf unbeantwortet** |
|
||||
| **Output-Scrubbing** | — | 📋 **#5** | Bekannte Secret-Werte aus dem Register des `SecretProtector` werden zentral aus **allen** Tool-Ergebnissen maskiert, bevor sie in Kontext, Historie, Audit-`Arguments` oder Spiegel gelangen. Ein Filter an `ExecuteToolCallAsync`. Schließt die Lücke, die S3 nur für URLs schloss |
|
||||
| Untrusted Content rahmen | K2-Rest | 📋 | Tool-Ergebnisse als Daten rahmen (`<untrusted_content>`) und im Kern-Prompt verankern, dass daraus keine Anweisungen befolgt werden. Staging nimmt die Schärfe, die Rahmung bleibt nötig — Finanzinhalte auf X und in Newslettern sind genau der Ort, an dem gezielt manipuliert wird |
|
||||
| Secret-UI | F-A6-Rest | 📋 | Oberfläche zum Setzen und Rotieren der Zugangsdaten. Die Verschlüsselung (S7, DPAPI) steht |
|
||||
| Audit-Export | — | 💭 | JSONL-Export für externe Auswertung. Passt zum A6-Spiegel |
|
||||
|
||||
### 3.2 — Agenten-Fähigkeiten
|
||||
|
||||
| Punkt | Herkunft | Stand | Was |
|
||||
|---|---|---|---|
|
||||
| Taskboard | A1, F-A5, F-A4 | ✅ | Markdown + YAML-Frontmatter im `SharedWorkspace`, Zustand in der DB. Scanner im 60-s-Takt statt Delay-Schleifen, atomares Claiming (at-most-once), Startup-Reconciliation, `blocked_by` mit Auto-Dispatch, Blocker-Eskalation, Reopen über `task_comment`. Assignee entscheidet: `@new`, `@<agent>`, `@human`. Bauplan: [Taskboard-Konzept](Taskboard-Konzept.md) |
|
||||
| Langzeitgedächtnis | K1 | ✅ | Typisierte Tabelle statt JSON im State-Store. Scope `agent`/`shared`, optionaler `Key` (erneutes Merken aktualisiert statt anzulegen), Wichtigkeit steuert die Reihenfolge bei Kappung. Bauplan: [Memory-Konzept](Memory-Konzept.md) |
|
||||
| **FileRW-Papierkorb** | — | 📋 **#3** | `delete` verschiebt in einen Papierkorb je Workspace statt endgültig zu löschen; ein Cron-Task räumt nach X Tagen auf. Danach kann `FileRW.delete` im Staging von `approve` auf `auto`. Bauplan: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-FileRW-Papierkorb-Cleanup.md) |
|
||||
| **A4 Skills/Toolsets** | T6 | 📋 **#7** | Dreischichtig statt „alles immer im System-Prompt": schlanker Dauerkern (hält den Prompt-Cache stabil), nachladbare Skills als `SKILL.md` mit Frontmatter (`list_toolsets` → `load_toolset`, bei vielen Skills stattdessen `skill_search`), Hot-Reload über `FileSystemWatcher` mit Debounce. Tool-Fehlermeldungen nennen die gültigen Parameter, statt das Modell raten zu lassen. **Agenten dürfen Skills aus Erfahrung selbst schreiben — aber erst nach Freigabe (A2) aktiv**, sonst ist es ein Injection-Kanal in künftige Läufe |
|
||||
| Memory-Flush vor Compaction | — | 📋 | Bevor der `ContextCompactor` zusammenfasst, bekommt der Agent ein eng begrenztes Fenster, Dauerhaftes per `memory_store` zu sichern — sonst wirft die Compaction Wissen weg. Harte Grenzen: max. 3–5 Schritte, einziges Tool `memory_store`, Timeout, günstiges Modell, höchstens einmal je Zyklus |
|
||||
| Memory-Auto-Injection | — | 📋 | Relevante Abstracts automatisch einblenden (Relevanzschwelle, Deckel ~200 Tokens). **In die Nutzernachricht, nie in den System-Prompt** — sonst verfällt der Prompt-Cache |
|
||||
| Proaktive Compaction | T4 | 📋 | `CompactIfNeededAsync` läuft heute **nach** dem API-Aufruf und nutzt die `promptTokens` der gerade bezahlten Anfrage — der überfüllte Prompt wurde also schon bezahlt. Vor dem Senden prüfen (`EstimateTokens` gibt es), gemessene Werte zur Kalibrierung |
|
||||
| AgentEditor härten | — | 📋 | Änderungen an `Identity.md`/`Soul.md` bleiben im laufenden Betrieb möglich, werden aber freigabepflichtig, nachvollziehbar und rücknehmbar. **Reihenfolge: nach AgentInspector.** Bauplan: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-AgentEditor-Haertung.md) |
|
||||
| AgentInspector | — | 📋 | Ein Supervisor-Agent kann beurteilen, ob die anderen tun, was sie sollen — über Audit-Log, Taskboard und lesenden Zugriff auf fremde Workspaces. Heute geht nur die Selbstauskunft, und die ist als Kontrollinstrument wertlos. Bauplan: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-AgentInspector-Supervisor.md) |
|
||||
| Memory-Verfall | — | 💭 | Alte, unwichtige Beobachtungen nach einer Frist entfallen lassen |
|
||||
| Task-Archivierung | — | 💭 | `done`/`canceled` nach einer Frist archivieren, damit `tasks/` nicht zuwächst |
|
||||
| Werkzeugwunsch | — | 💭 | Agenten bekommen in den Kern-Prompt den Hinweis, sich zu melden, wenn ihnen ein Werkzeug fehlt. Der Weg ist da — ein Task an `@human`. Gehört zum Prompt-Kern von A4 |
|
||||
|
||||
### 3.3 — Kommunikation zwischen Agenten
|
||||
|
||||
Bauplan: [Agentenkommunikation-Konzept](Agentenkommunikation-Konzept.md). **Nichts davon
|
||||
ist gebaut** — mit einer Ausnahme: Die Ansicht (Phase 4) steht bereits als
|
||||
`AgentChatsPageView` und wartet nur auf den Datenbestand aus Phase 2.
|
||||
|
||||
| Punkt | Herkunft | Stand | Was |
|
||||
|---|---|---|---|
|
||||
| **Delegationstiefe begrenzen** | B8 | 📋 **#4** | `AgentComm.send_message` ist ein synchroner Aufruf ohne Tiefenbegrenzung. A→B→A verklemmt echt: A hält sein Gate, während es auf B wartet — ruft B nun A, wartet B auf As Gate. Löst nur der Timeout auf. Test A13 der [Teststrategie](Teststrategie.md) deckt genau das ab und **fehlt bis heute** |
|
||||
| Phase 1 — Korrelation | — | 📋 | `ParentRunId` + `RootRunId` in Audit-Log und Receipts, von der Engine gestempelt. Eine Delegationskette wird damit zu einer Abfrage. Voraussetzung für alles Weitere |
|
||||
| Phase 2 — Nachrichtenspeicher | — | 📋 | Tabelle `AgentMessages` + FTS5, alle Wege gleich behandelt, beide Richtungen festgehalten. **Erst nach dem Output-Scrubbing** (#5) |
|
||||
| Phase 3 — `ask_agent` | — | ❄️ | Tiefe 1 mit Zyklusprüfung, Rückbau von `AgentComm`/`AgentSpawn`. Wartet auf E4 |
|
||||
| Phase 4 — Paar-Ansicht | — | ✅/❄️ | Auswahl, Filterung und Darstellung sind fertig; es fehlt genau eine Abfrage (`AgentChatsPageViewModel.LoadMessages`), sobald Phase 2 steht |
|
||||
| Phase 5 — `AgentCommAnalysis` | — | 📋 | Analyse-Tool, das findet, wo die Zusammenarbeit klemmt. Wer es bekommt, ist offen |
|
||||
| `@human`-Tasks mitschreiben | — | 💭 | Gäbe die Paar-Ansicht auch für Mensch↔Agent, fast ohne Zusatzaufwand. Vorschlag: ja, aber nach Phase 4 |
|
||||
|
||||
### 3.4 — Daten und Speicher
|
||||
|
||||
| Punkt | Herkunft | Stand | Was |
|
||||
|---|---|---|---|
|
||||
| Atomares Schreiben | — | ✅ | `File.Replace`-Muster. Es war bereits eine `TokenUsage.json` beschädigt worden |
|
||||
| Speicher-Fundament | — | ✅ | WAL, `busy_timeout`, Pooling, Schreib-Warteschlange im Prozess. Vorher gab es `database is locked` — das sah nach einer SQLite-Grenze aus, war aber fehlende Konfiguration |
|
||||
| Sicherung + Wiederherstellung | — | ✅ | ZIP mit Manifest und Prüfsummen, `VACUUM INTO` für die DB, Secrets wahlweise auf Passphrase umgeschlüsselt (PBKDF2 + AES-GCM) oder ausgelassen — DPAPI allein überlebt den Rechnerwechsel nicht, und genau dann braucht man das Backup. Rotation, Restore mit Vorschau, Zeitplan |
|
||||
| **Historie-Umzug** | K6, B10 | 📋 **#8** | `ChatHistory.json` zieht in die Instanz-DB: aktive Tabelle + Archiv-Tabelle mit FTS5 und `history_search`-Tool. Löst zugleich B10 — heute wird bei **jedem** Chat-Eintrag die vollständige Historie und der vollständige Kontext als eingerücktes JSON neu geschrieben (O(n²); bei 500 Nachrichten mehrere MB pro Nachricht). Migration nur nach frischem Backup, alte JSON erst nach verifiziertem Import löschen |
|
||||
| A6 MySQL-Spiegel | — | ❄️ | **SQLite bleibt die einzige Wahrheit.** MySQL ist reiner nachgelagerter Spiegel: empfängt nur vom Replikator, die App liest im Betrieb **nie** daraus. Outbox-Muster an der vorhandenen Schreib-Warteschlange, idempotente Upserts, nie blockierend. Zeilen tragen `instance_id`. **Der Restore-Pfad Spiegel → frische SQLite muss existieren und getestet sein** — gleiche Regel wie beim Backup. Wartet auf den Historie-Umzug und auf E1 (der Server kommt ggf. mit A5) |
|
||||
| MySQL-Gedächtnis | — | 💭 | Zweite `IMemoryRepository`-Implementierung, wenn mehrere Rechner dazukommen |
|
||||
|
||||
### 3.5 — Betrieb und Auslieferung
|
||||
|
||||
WatchDog und LicenseLabrador sind durch das
|
||||
[Deploymentcenter](Deploymentcenter-Integration.md) ersetzt: eine Adresse, ein Token,
|
||||
und dazu Update-Prüfung, Fehler-Stream und Bugtracker. Watchdog läuft wieder pro
|
||||
Instanz (ein Monitor je Instanz, mit Gesundheitsprüfungen und angekündigtem Ende).
|
||||
dazu Update-Prüfung, Fehler-Stream und Bugtracker. Watchdog läuft pro Instanz.
|
||||
|
||||
Offen:
|
||||
| Punkt | Herkunft | Stand | Was |
|
||||
|---|---|---|---|
|
||||
| Zugangsschutz | DC-2.4 | ✅ | Lizenzschlüssel als Basic-Auth-Zugang zur Release-Ablage. Benutzername wird als `lic_<sha256[0..16]>` abgeleitet — **die Ableitung muss auf beiden Seiten zeichengenau übereinstimmen, wer eine Seite ändert, sperrt die gesamte Installationsbasis aus** |
|
||||
| Update anwenden | DC6 | ✅ | Agent liegt im Paket (Prüfsumme geprüft), Rückfrage in der Oberfläche, geordnetes Herunterfahren vor dem Agentenstart, `maintenance` an den Watchdog |
|
||||
| Erstinstallation | DC7 | ✅ | `--action install` + `setup.json`, live durchgespielt (Login, Katalog, Token mit Rechteschranke) |
|
||||
| Release-Strecke | DC3 | ✅/📋 | Erledigt für `win-x64`/`dev` ([`deploy/publish.py`](../deploy/publish.py)). **Offen:** `linux-x64` — die Portierung, auf die das wartete, ist durch — und `prod` |
|
||||
| **DC1 „Fehler melden"** | DC1 | 📋 | Der Client steht, es fehlt die Schaltfläche in der Oberfläche |
|
||||
| DC2 Bugtracker-Tool | DC2 | 📋 | Agenten-Tool, das den Claim/Lease-Workflow des Deploymentcenters nutzbar macht |
|
||||
| DC4 SDK als Submodul | DC4 | 📋 | Heute ein Cross-Repo-Pfad (`..\..\..\Deploymentcenter\…` in `ClawdDotNet.App.csproj`) — auf einem anderen Rechner baut das nicht. Als Git-Submodul unter `external/`. Betrifft auch die CI |
|
||||
| DC5 Evaluator-Cron | DC5 | 📋 | Betreiberaufgabe: Cron einrichten, Token ausstellen, `parent_source` pflegen. **Ohne den Cron ist die Überwachung wertlos** |
|
||||
| DC8 Signaturpflicht | DC8 | ❄️ | Schlüssel steht, ab 0.1.2 wird signiert, Prüfung beidseitig getestet. **Blockiert:** `--require-signature` gibt es nur als Kommandozeilenschalter; `UpdateClient.LaunchUpdateAgent` — der empfohlene Weg — reicht ihn nicht durch. Selbst nachbauen hieße `--restart`, `--wait-for-pid` und `--wait-timeout` verlieren. Gemeldet |
|
||||
| Release-Guard beim Upload | — | 📋 | Veröffentlichen löst `regenerateForProject` nicht aus; ein frisches Produktverzeichnis steht bis zum nächsten Sechs-Stunden-Turnus ohne `.htaccess` da. Ein Aufruf am Ende von `publish` würde das Fenster schließen. **Betreiberseite** |
|
||||
| Linux-Betrieb | — | 📋 | Der Kern ist portabel: alle Bibliotheks- und Testprojekte auf `net10.0`, kein `DllImport`, keine Registry, kein `System.Drawing`. Windows steckt noch an drei Stellen — DPAPI, Groß-/Kleinschreibung bei Pfadvergleichen, Zeitzonen-IDs. Der teure Teil (8.900 Zeilen WinForms) ist mit der Avalonia-Portierung entfallen |
|
||||
| systemd-Dienst | — | 💭 | Kopfloser Betrieb. `ClawdDotNet.App` läuft bereits ohne Fenster — der Schichtschnitt ist genau dafür da. `NonInteractiveLicensePrompt` und `ConsoleLicensePrompt` liegen ungenutzt bereit |
|
||||
|
||||
| Punkt | Was | Bemerkung |
|
||||
### 3.6 — Finanz und Analyse
|
||||
|
||||
Dieser Block ist bewusst **nachgelagert**: ClawdDotNet soll zuerst als allgemeiner
|
||||
Agenten-Client felsenfest sein, die handelsspezifischen Werkzeuge kommen danach.
|
||||
|
||||
| Punkt | Herkunft | Stand | Was |
|
||||
|---|---|---|---|
|
||||
| **C1 Marktkalender** | C1 | 📋 **#6** | Zwei Bausteine: `onlyWhenMarketOpen: "NYSE"` im Task-Frontmatter (der Lauf wird schlicht übersprungen, wirkt ohne Zutun des Modells) und ein `MarketCalendar`-Tool für Fragen des Agenten. Handelskalender ändern sich selten — eine gepflegte Datei reicht, keine externe Abhängigkeit. Der Haken im Scanner ist da |
|
||||
| C2 Indicators | C2 | 📋 | Gleitende Durchschnitte, RSI, ATR, Volatilität, Korrelation, Drawdown, Positionsgröße nach Risiko — **im Code gerechnet**. Sprachmodelle rechnen unzuverlässig, und die Zahlenkolonnen müssen dafür durch den Kontext. Spart Tokens *und* verbessert die Qualität |
|
||||
| C3 Datenaktualität | C3 | 📋 | `DirectAPI` liefert `dataAsOf` mit, aber nichts wertet es aus — ein Agent kann ungehindert auf drei Tage alten Kursen argumentieren. `maxAgeSeconds` je Tool: ablehnen oder unübersehbar kennzeichnen, nicht stillschweigend durchreichen |
|
||||
| C4 Termine und Fundamentaldaten | C4 | 📋 | Earnings, Dividenden, Splits, SEC EDGAR (8-K, 10-Q, 13F — frei und gut strukturiert), Wirtschaftstermine. Für „Finanznachrichten" ist der Kalender oft wichtiger als der Kurs |
|
||||
| C5 Bestandsregister | C5 | 📋 | `stock_add` ist eine Wissenssammlung, kein Bestand. Eigene Tabelle mit Positionen, auch rein zur Beobachtung. Grundlage für C7/C8 |
|
||||
| C6 Nachrichten-Entdopplung | C6 | 📋 | Dieselbe Meldung läuft über zehn Quellen — man zahlt zehnmal, und der Agent hält es für zehn unabhängige Signale. Das verzerrt die Einschätzung systematisch. `SeenItems` mit Prüfsumme über den normalisierten Titel plus Ähnlichkeitsabgleich |
|
||||
| C7 Ergebnisregister | C7 | 📋 | Tabelle `AgentOutput` am Lauf: Art, Betreff, Verweis. Macht aus „Kosten pro Lauf" die nützlichere Größe **Kosten pro Ergebnis**. Fällt weitgehend aus den A3-Receipts ab |
|
||||
| C8 Falsifizierbare Aussagen | C8 | 📋 | Das eigentliche Leistungsmaß. Der Agent legt sich fest (Subjekt, Aussage, Horizont, Konfidenz); ein Auflösungs-Task prüft gegen die tatsächlichen Kurse — kein Mensch muss bewerten. Daraus Trefferquote, **Brier-Score** (misst, ob die Konfidenz ehrlich war — ein Agent, der bei 0,9 nur in 60 % recht hat, ist überheblich, das bleibt bei reiner Trefferquote unsichtbar) und Vergleich gegen eine Nulllinie. **Unter 30 aufgelösten Aussagen keine Rangliste, nur Rohzahlen** — sonst optimiert man Rauschen. Braucht C5 und C7 |
|
||||
| Leerlaufquote | — | 📋 | Die wirksamste einfache Kennzahl: Läufe ohne greifbares Ergebnis. 40 % Leerlauf heißt meist Zeitplan-Problem — genau das, was C1 löst. Aus vorhandenen Daten |
|
||||
| `stock_add` herauslösen | T6 | 📋 | Fachfremdes Feature, das das `FileRW`-Schema für alle aufbläht, auch für Agenten, die es nie nutzen |
|
||||
| Orderausführung | — | 💭 | Siehe E9. Eigene Kategorie, ausdrücklich kein Nebenprodukt |
|
||||
|
||||
### 3.7 — Hygiene
|
||||
|
||||
Kleinbugs, in einem Aufwasch zu erledigen.
|
||||
|
||||
| Punkt | Herkunft | Stand | Was |
|
||||
|---|---|---|---|
|
||||
| `index_Count`-Race | B9 | 📋 | `FileRWTool` ruft `index_Count` auf, nachdem das Lock freigegeben ist — Race mit parallelen `stock_add`, plus ein überflüssiges vollständiges Parsen. Der Zähler ist innerhalb des Locks ohnehin bekannt |
|
||||
| `instanceId`-Inkonsistenz | B13 | 📋 | `RunAsync`/`ChatAsync` bekommen `instanceId` als Parameter, `SendMessageAsync`/`SpawnAgentAsync` nutzen das Feld `_instanceId`, das nur bei `SetAgentConfigProvider` gesetzt wird — sonst leer. In Produktion unkritisch |
|
||||
| DST-Randfall | — | 📋 | Eine bei der Zeitumstellung nicht existierende Ortszeit („02:30" im Frühjahr) wird übersprungen statt verschoben. Tests R4/R5 der [Teststrategie](Teststrategie.md) |
|
||||
| Nullable-Warnungen | — | 📋 | 45 Build-Warnungen, Schwerpunkt `Mail` (basisnah) und `WebFetch`/`DirectAPI` (handelsnah) |
|
||||
| Paket-Anhebungen | — | ✅ | `SQLitePCLRaw.bundle_e_sqlite3` 2.1.13 und `SharpCompress` 0.48.0 stehen als **direkte Verweise, obwohl kein Code sie aufruft** — sie heben verwundbare transitive Fassungen an. Sehen aus wie toter Ballast und sind es nicht |
|
||||
|
||||
### 3.8 — Tests
|
||||
|
||||
Grundlage: [Teststrategie](Teststrategie.md). Stand: 585 grün, 6 übersprungen.
|
||||
|
||||
| Punkt | Stand | Was |
|
||||
|---|---|---|
|
||||
| DC1 | Oberfläche „Fehler melden" | Client vorhanden, Schaltfläche fehlt |
|
||||
| DC2 | Agenten-Tool für den Bugtracker | macht den Claim/Lease-Workflow des Deploymentcenters nutzbar |
|
||||
| DC3 | Release-Strecke | **erledigt für win-x64/dev** ([`deploy/publish.py`](../deploy/publish.py), 0.1.1 veröffentlicht). Offen: `linux-x64` nach der Avalonia-Portierung, `prod` |
|
||||
| DC4 | SDK als Git-Submodul unter `external/` statt Cross-Repo-Pfad | betrifft auch die CI |
|
||||
| DC5 | Betreiber: Evaluator-Cron einrichten, Token ausstellen, `parent_source` pflegen | **ohne den Cron ist die Überwachung wertlos** |
|
||||
| DC6 | Update anwenden statt nur melden | **erledigt.** Agent liegt im Paket (Prüfsumme geprüft), Rückfrage in der Oberfläche, geordnetes Herunterfahren vor dem Agentenstart, `maintenance` an den Watchdog |
|
||||
| DC7 | Erstinstallation über `--action install` + `setup.json` | **erledigt.** Setup-Kette live durchgespielt (Login, Katalog, Token mit Rechteschranke), Round-Trip SDK-Writer → SettingsManager trägt. `fileWindows`/`fileLinux` gesetzt |
|
||||
| DC8 | Signaturpflicht (`--require-signature`) | Schlüssel steht, ab 0.1.2 wird signiert, Prüfung beidseitig getestet. **Blockiert:** `LaunchUpdateAgent` reicht den Schalter nicht durch — aus der Anwendung heraus nicht erzwingbar (gemeldet) |
|
||||
|
||||
Details: [Deploymentcenter 2.2–2.4 Integrationsplan](Deploymentcenter-2.4-Integrationsplan.md).
|
||||
Der Zugangsschutz aus 2.4 (Lizenzschlüssel als Basic-Auth-Zugang zur Release-Ablage) ist
|
||||
bereits umgesetzt — er muss im **ersten** veröffentlichten Release enthalten sein.
|
||||
| **A13** | 📋 | `send_message` A→B→A wird nach `maxDelegationDepth` abgebrochen statt zu verklemmen. Gehört zwingend zu Reihenfolge-Punkt #4 |
|
||||
| R4/R5 | 📋 | Zeitumstellung Oktober (feuert genau einmal) und März (wird nicht übersprungen) |
|
||||
| Oberfläche | — | Hat **keine** Testabdeckung. Die leisen Fehler (Fäden, unbegrenzt wachsende Listen, nicht behandeltes Fenster-Schließen) fallen ausschließlich beim Laufen auf — Abschnitt 4 des [Oberflächen-Leitfadens](Oberflaechen-Leitfaden.md) ist deshalb Pflichtlektüre vor jeder Abgabe |
|
||||
|
||||
---
|
||||
|
||||
## C — Offen aus dem Finanz-/Analyse-Konzept
|
||||
## 4 — Ideenspeicher
|
||||
|
||||
Punkte 1–2 von dort (atomares Schreiben, Backup/Restore inkl. UI) sind umgesetzt.
|
||||
Nicht beschlossen. Steht hier, damit es nicht verlorengeht — nicht, weil es demnächst
|
||||
gebaut wird.
|
||||
|
||||
| # | Was | Bemerkung |
|
||||
### Tool-Kandidaten
|
||||
|
||||
| Tool | 💭 Nutzen | Bemerkung |
|
||||
|---|---|---|
|
||||
| C1 | Marktkalender (`onlyWhenMarketOpen` + `MarketCalendar`-Tool) | Scheduler-Teil gehört ins Taskboard-Frontmatter (A1) |
|
||||
| C2 | `Indicators`-Tool — deterministische Berechnung | Qualität hoch, Tokens runter |
|
||||
| C3 | Datenaktualität erzwingen (`maxAgeSeconds`) | |
|
||||
| C4 | Termine & Fundamentaldaten (Earnings, EDGAR, Wirtschaftskalender) | |
|
||||
| C5 | Bestandsregister (Positionen) | Grundlage für C7/C8 |
|
||||
| C6 | Nachrichten-Entdopplung (`SeenItems`) | |
|
||||
| C7 | Ergebnisregister (Stufe 2, „Kosten pro Ergebnis") | fällt weitgehend aus den A3-Receipts ab |
|
||||
| C8 | Falsifizierbare Aussagen + Auflösung, Brier-Score (Stufe 3) | braucht C5, C7 und einen Auflösungs-Task (A1) |
|
||||
| **WebSearch** | Agenten können heute nur bekannte Domains abrufen, aber nichts *finden*. Brave/Tavily/SearXNG | Bauplan liegt vor: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-WebSearch-Tool.md). **Ausdrücklich zuletzt** — größter Sicherheitshebel, deshalb erst nach Papierkorb, Inspector und AgentEditor-Härtung |
|
||||
| **Nextcloud** | Ort, an dem Agenten Ergebnisse ablegen. Datei lokal erzeugen → hochladen → Link zurück in den Chat | Nextcloud hat keine API, die Inhalte *erzeugt*. Für PDF/XLSX kann Collabora als Konverter dienen. Bauplan: [RocketChat-Nextcloud-Konzept §5](RocketChat-Nextcloud-Konzept.md). Lohnt sich unabhängig vom Ausgang von E1 |
|
||||
| **Http** | Generisches REST-Tool mit Allowlist je Agent | `DirectAPI` ist fest auf Finanz-Provider verdrahtet — jede neue API erfordert heute Code |
|
||||
| **Shell** | Sandboxed, mit Kommando-Allowlist | Ersetzt die hartcodierten yt-dlp/ffmpeg-Aufrufe |
|
||||
| **Git** | Für den „Senior Developer"-Agenten | |
|
||||
| **Vision** | Charts und Screenshots analysieren | |
|
||||
| ~~Notify~~ | — | **Gestrichen**, geht in A5 auf |
|
||||
|
||||
### Weitere Ideen
|
||||
|
||||
| Idee | 💭 |
|
||||
|---|---|
|
||||
| Rocket.Chat Realtime/DDP statt Polling | Antwortzeit ~1 s statt ~30 s. Optional, Phase 6 |
|
||||
| Collabora-Konvertierung | Berichte in Büroformaten. Hängt an E-Frage 5 (ist der `convert-to`-Endpunkt freigebbar?) |
|
||||
| `ChannelRouter` + Eskalation | Erkennt den Ausfall des Chat-Kanals und weicht auf Telegram aus. **Kein Nice-to-have**, sobald der Chat der Hauptweg ist — sonst ist er ein Einzelpunkt, dessen Ausfall niemand meldet. Hängt an E1 |
|
||||
| K4 Streaming | ❄️ Zurückgestellt bis E1 entschieden ist — Matrix streamt nicht |
|
||||
|
||||
---
|
||||
|
||||
## D — Toolkandidaten (unbeschlossen)
|
||||
## 5 — Erledigt
|
||||
|
||||
WebSearch, Http (generisch mit Allowlist), Shell (sandboxed), Git, Vision.
|
||||
Notify ist gestrichen — geht in A5 auf.
|
||||
Damit die Chronik nicht mit den alten Papieren verschwindet.
|
||||
|
||||
**Sicherheit:** S1 `SqlGuard` statt Teilzeichenketten-Prüfung · S2 `YouTubeUrl` +
|
||||
`ArgumentList` gegen Options-Injection · S3 `UrlSanitizer` gegen API-Key-Leak ins Modell ·
|
||||
S4 `PermissionGate` zum Durchsetzungspunkt ausgebaut (in A2 aufgegangen) · S5 `UrlGuard`,
|
||||
Redirects einzeln geprüft · S6 `WorkspacePath` auf Verzeichnisgrenzen · S7
|
||||
`SecretProtector` (DPAPI).
|
||||
|
||||
**Bugs:** B1/B14 Compaction — sicherer Schnittpunkt, kein doppelter System-Prompt ·
|
||||
B2 Chat-Läufe je Agent serialisiert · B3/T5 `maxCumulativeTokens` von `maxContextTokens`
|
||||
getrennt · B4 Prompt/Completion getrennt erfasst, Preise live vom `/models`-Endpunkt ·
|
||||
B5/T2 Tool-Ergebnisse zentral gekappt · B6/B7 gegenstandslos, seit der Scanner die
|
||||
Delay-Schleifen ersetzt · B11/T8 `max_tokens` aus `LoopGuard.MaxResponseTokens` ·
|
||||
B12 Retry mit Backoff für 429/5xx.
|
||||
|
||||
**Konzepte:** K1 Gedächtnis · K3 Testfundament · K5 Budget-Guard (F-A3) · A1 Taskboard
|
||||
(mit F-A4, F-A5) · A3 Audit-Log (F-A2) · A2-Kern.
|
||||
|
||||
**Token:** T1/T9 Prompt-Caching mit Breakpoints, `cached_tokens` gemessen · T3 günstiges
|
||||
Modell für die Zusammenfassung · T7 durch die Assignee-Semantik des Taskboards
|
||||
beantwortet.
|
||||
|
||||
**Betrieb:** Alt-Scheduler abgelöst — `AgentScheduler`, `ToolJobScheduler` und
|
||||
`SchedulerDelay` gelöscht, alles ist ein Task · Atomares Schreiben · Backup und Restore
|
||||
inkl. Oberfläche · Deploymentcenter statt WatchDog und LicenseLabrador · DC3 (win-x64),
|
||||
DC6, DC7, Zugangsschutz 2.4 · Avalonia-Portierung, WinForms entfernt.
|
||||
|
||||
**Frühjahrsputz 2026-08-23:** automatische Sicherung war nie gestartet · doppelte
|
||||
Sicherungs-Einstellungen auf der Einstellungsseite überschrieben die der Sicherungsseite ·
|
||||
NU1903 und NU1902 geschlossen.
|
||||
|
||||
---
|
||||
|
||||
## Vorgeschlagene Reihenfolge
|
||||
## 6 — Modell-Einstufung
|
||||
|
||||
| # | Vorhaben | Begründung |
|
||||
Die Entwicklung läuft mit Opus 4.6. Die meisten Vorhaben sind damit gut machbar, sofern
|
||||
die hier notierten Vorgaben mitgegeben werden. Die zwei ursprünglich für Opus 5 / Fable
|
||||
markierten Stellen sind inzwischen gebaut — die Begründung bleibt stehen, weil sie
|
||||
erklärt, *warum* es so gebaut ist.
|
||||
|
||||
| Vorhaben | Einstufung | Vorgabe |
|
||||
|---|---|---|
|
||||
| 1 | A1 Taskboard | Fundament; löst sechs bestehende Punkte auf einmal |
|
||||
| 2 | A3 Audit-Log | klein, sofort nützlich; muss vor A2 da sein, damit Freigaben protokolliert werden |
|
||||
| 3 | A2 Staging-Freigabe | größter Sicherheitsgewinn; Voraussetzung für unbeaufsichtigten Betrieb |
|
||||
| 4 | C1 Marktkalender | spart sofort Kosten; nutzt A1-Frontmatter |
|
||||
| 5 | A4 Skills/Toolsets | Token-Hebel, Cache-stabil |
|
||||
| 6 | C2 Indicators | Qualität + Kosten |
|
||||
| 7 | C7 + C8 Ergebnisregister, Aussagen | das eigentliche Leistungsmaß; braucht A3 |
|
||||
| — | Hygiene-Paket (B) | zwischendurch, unabhängig |
|
||||
| — | Historie-Umzug in die Instanz-DB | löst B10 + K6; Voraussetzung für A6 |
|
||||
| — | A6 MySQL-Spiegel | nach dem Historie-Umzug; natürliches Zuhause auf dem A5-Server |
|
||||
| — | A5 Matrix | eigenes Konzept-Doc zuerst; Scope klären, dann einordnen |
|
||||
| ✅ A1 Scanner-Kern | war ⚠️ Opus 5 / Fable | At-most-once, Claim-CAS, Zusammenspiel mit den serialisierten Chat-Läufen — genau die Fehlerklasse, die hier schon einmal schiefging. Gebaut mit Property-Tests für die Invarianten |
|
||||
| ✅ A2 Fortsetzung nach Freigabe | 4.6 mit Vorgabe | **Kein pausierter, im Speicher gehaltener Lauf.** Der Lauf endet beim Staging regulär, die Freigabe erzeugt einen Folge-Task mit dem eingefrorenen Aufruf. Echtes Suspend/Resume wäre Fable-Terrain — und ist damit unnötig |
|
||||
| Freigabe-Ansicht | 4.6 | Reine Oberflächenarbeit gegen eine fertige API |
|
||||
| Delegationstiefe (B8) | ⚠️ **mit Vorsicht** | Fasst das Agent-Gate an, an dem schon ein Deadlock lauerte. Erst Invarianten festlegen, dann bauen, Test A13 zwingend |
|
||||
| Output-Scrubbing | 4.6 | Ein zentraler Filter an `ExecuteToolCallAsync`, Werte aus dem Secret-Register |
|
||||
| A4 Skills | 4.6 | `FileSystemWatcher` mit Debounce (~500 ms); agentengeschriebene Skills erst nach Freigabe aktiv |
|
||||
| Memory-Flush vor Compaction | 4.6 mit Anleitung | Der `ContextCompactor` hatte B1/B14 — die bestehenden Paarungs-Tests müssen unverändert grün bleiben |
|
||||
| Historie-Umzug + FTS5 | 4.6 | Migration nur nach frischem Backup; alte JSON erst nach verifiziertem Import löschen |
|
||||
| A6 MySQL-Spiegel | 4.6 mit Anleitung | Outbox mit Wasserzeichen, idempotente Upserts, nie blockieren. **Der getestete Restore-Pfad ist Teil der Definition of Done** |
|
||||
| C1, C2 | 4.6 | Reine Fachlogik, deterministisch testbar |
|
||||
|
||||
Leitlinie der Reihung: erst Nachvollziehbarkeit und Kontrolle (Audit, Staging),
|
||||
dann Fähigkeiten — ein Agent, der unbeaufsichtigt läuft, braucht zuerst Bremsen,
|
||||
dann PS.
|
||||
Generell: Neue Subsysteme kommen mit Tests nach der [Teststrategie](Teststrategie.md).
|
||||
Bei den markierten Punkten sind die Invarianten-Tests kein Nice-to-have, sondern die
|
||||
Absicherung dafür, dass ein schwächeres Modell sie umsetzen darf.
|
||||
|
||||
---
|
||||
|
||||
## Umsetzung mit Opus 4.6 — Einstufung
|
||||
## 7 — Woher das hier kommt
|
||||
|
||||
Die Entwicklung erfolgt mit Opus 4.6. Die meisten Vorhaben sind damit gut
|
||||
machbar, sofern die hier notierten Vorgaben mitgegeben werden. Zwei Stellen
|
||||
berühren Nebenläufigkeits-Invarianten bzw. Engine-Querschnitte — sie sind für
|
||||
Opus 5 / Fable markiert oder durch eine Architektur-Vorgabe entschärft.
|
||||
### Weiter gültig — Baupläne
|
||||
|
||||
| Vorhaben | Einstufung | Vorgabe / Begründung |
|
||||
|---|---|---|
|
||||
| A1: Dateiformat, Frontmatter-Parsing, `task_*`-Tool, Migration | 4.6 | klar spezifizierbar, gut testbar |
|
||||
| A1: **Scanner-Kern** (atomares Claiming, Auto-Dispatch, Reconciliation) | ⚠️ **Opus 5 / Fable** | At-most-once-Semantik, Claim-CAS und das Zusammenspiel mit den seit B2 serialisierten Chat-Läufen sind genau die Fehlerklasse, die hier schon einmal schiefging. Falls doch 4.6: erst Konzept-Doc, Umsetzung strikt dagegen, Property-Tests für die Invarianten („nie zwei Claims auf einen Task", „kein Dispatch bei offenem Blocker", „doppelter Tick = ein Lauf") |
|
||||
| A2: Gate, Policy, Staging-Queue, Review-UI, Approval-Records | 4.6 | mit der folgenden Architektur-Vorgabe |
|
||||
| A2: **Fortsetzung nach Freigabe** | 4.6 nur mit Vorgabe | **Kein pausierter, im Speicher gehaltener Lauf.** Vorgabe: Der Lauf endet beim Staging regulär — das Tool liefert „zur Freigabe vorgelegt" als Ergebnis, der Agent schließt ab. Die Freigabe erzeugt einen Folge-Task (A1), der den Agenten mit dem **eingefrorenen** Aufruf weckt. Echtes Suspend/Resume eines laufenden `ChatAsync` wäre Fable-Terrain — und ist mit dieser Vereinfachung unnötig |
|
||||
| A3: Audit-Log + Receipts | 4.6 | append-only, klares Schema, keine Nebenläufigkeitsfallen |
|
||||
| A4: Skills | 4.6 | `FileSystemWatcher` mit Debounce (~500 ms); agentengeschriebene Skills erst nach Freigabe aktiv (siehe A4) |
|
||||
| Memory-Flush vor Compaction | 4.6 mit Anleitung | Harte Grenzen: max. 3–5 Schritte, einziges Tool `memory_store`, Timeout, günstiges Modell (wie T3), höchstens einmal je Compaction-Zyklus. Vorsicht: Der ContextCompactor hatte B1/B14 — die bestehenden Paarungs-Tests müssen unverändert grün bleiben |
|
||||
| Memory-Auto-Injection | 4.6 | in die Nutzernachricht, nie in den System-Prompt (sonst verfällt der Prompt-Cache T1); Deckel ~200 Tokens |
|
||||
| Output-Scrubbing | 4.6 | ein zentraler Filter an der Stelle, wo Tool-Ergebnisse in den Kontext gelangen (`ExecuteToolCallAsync`); Werte aus dem Secret-Register |
|
||||
| Historie-Umzug + FTS5 + `history_search` | 4.6 | Migration nur nach frischem Backup; alte JSON-Dateien erst nach verifiziertem Import löschen |
|
||||
| A6: MySQL-Spiegel | 4.6 mit Anleitung | Outbox mit Wasserzeichen, idempotente Upserts, nie blockieren; der getestete Restore-Pfad ist Teil der Definition of Done |
|
||||
| C1 Marktkalender, C2 Indicators | 4.6 | reine Fachlogik, deterministisch testbar |
|
||||
Aus der Roadmap verlinkt. Wer einen Punkt anfasst, liest das zugehörige Dokument.
|
||||
|
||||
Generell: Neue Subsysteme (Scanner, Staging, Audit, Replikator) kommen mit Tests
|
||||
nach der [Teststrategie](Teststrategie.md) — die Invarianten-Tests sind bei den
|
||||
markierten Punkten kein Nice-to-have, sondern die Absicherung dafür, dass ein
|
||||
schwächeres Modell sie umsetzen darf.
|
||||
| Dokument | Wofür |
|
||||
|---|---|
|
||||
| [Taskboard-Konzept](Taskboard-Konzept.md) | Dateiformat, Wahrheitsaufteilung Datei/DB, Scanner-Verhalten, Invarianten |
|
||||
| [Audit-Konzept](Audit-Konzept.md) | Schema, Provenance-Regeln, Receipts |
|
||||
| [Staging-Konzept](Staging-Konzept.md) | Policy-Auflösung, Plan-Freeze, Fortsetzung nach Freigabe — **Bauplan für Punkt #1** |
|
||||
| [Memory-Konzept](Memory-Konzept.md) | Modell, Schlüssel-Semantik, Abrufreihenfolge |
|
||||
| [Agentenkommunikation-Konzept](Agentenkommunikation-Konzept.md) | Fünf Phasen, Korrelation, Nachrichtenspeicher |
|
||||
| [RocketChat-Nextcloud-Konzept](RocketChat-Nextcloud-Konzept.md) | Rocket.Chat gebaut; Nextcloud, Rückweg und `ChannelRouter` offen. Enthält die Messung gegen die echte Instanz |
|
||||
| [Deploymentcenter-Integration](Deploymentcenter-Integration.md) | Wie Heartbeat, Fehler-Stream, Bugtracker und Updates verdrahtet sind |
|
||||
| [umsetzungsplaene/](umsetzungsplaene/) | Vier Detailpläne: FileRW-Papierkorb, AgentInspector, AgentEditor-Härtung, WebSearch |
|
||||
|
||||
### Weiter gültig — Handbücher
|
||||
|
||||
| Dokument | Wofür |
|
||||
|---|---|
|
||||
| [Oberflaechen-Leitfaden](Oberflaechen-Leitfaden.md) | Arbeiten an `ClawdDotNet.Desktop`: Schichtschnitt, Fäden, Avalonia-12-Fallen, Prüfliste |
|
||||
| [ToolDevelopmentGuide](ToolDevelopmentGuide.md) | Ein neues Agenten-Tool bauen |
|
||||
| [InstanceSetupGuide](InstanceSetupGuide.md) | Eine Instanz einrichten |
|
||||
| [Teststrategie](Teststrategie.md) | Was wie getestet wird, Fallkatalog |
|
||||
|
||||
### Archiviert
|
||||
|
||||
Vollständig in dieses Dokument überführt. Sie bleiben als Begründung und Herkunft
|
||||
lesbar, werden aber **nicht mehr fortgeschrieben** — siehe [docs/archiv/](archiv/).
|
||||
|
||||
| Dokument | Was daraus wurde |
|
||||
|---|---|
|
||||
| `Bestandsaufnahme-2026-07.md` | S1–S7, B1–B14, K1–K6, T1–T9, F-A1…F-A7 — erledigte in Abschnitt 5, offene in 3.1/3.2/3.7 |
|
||||
| `Konzepte-Backup-Finanz-Analyse.md` | Backup ✅; Finanzteil als C1–C6; Leistungsanalyse als C7/C8 und Leerlaufquote |
|
||||
| `Linux-Portierung-Analyse.md` | Die drei Kernstellen und das `linux-x64`-Release in 3.5. Der teure Teil (WinForms) ist entfallen |
|
||||
| `Lizenz-HardwareId-v2-Implementierungsvorschlag.md` | **Gegenstandslos** — LicenseLabrador ist durch das Deploymentcenter ersetzt |
|
||||
| `Deploymentcenter-Anbindung-Review.md` | Befunde behoben, in 3.5 aufgegangen |
|
||||
| `Deploymentcenter-2.4-Integrationsplan.md` | DC-Punkte in 3.5, inklusive der blockierten Signaturpflicht und der Betreiber-Restpunkte |
|
||||
| `ClawdDotNet_StartPrompt.md` und die drei Prompt-Anhänge | Entwicklungs-Prompts der Anfangszeit. Eine Regel gilt weiter: die Pflichtfelder `fetchedAt`/`dataAsOf`/`source` der Internet-Tools |
|
||||
|
||||
@@ -6,7 +6,14 @@ Der typische Ablauf ist die Kombination aus beidem — „schreib mir die Auswer
|
||||
sie in die Cloud" im Chat, Datei in Nextcloud, Link zurück in den Chat.
|
||||
|
||||
Dieses Dokument prüft die Machbarkeit, legt den Schnitt fest und benennt die Punkte, die
|
||||
vor der Umsetzung entschieden werden müssen. **Es ist noch keine Umsetzungsfreigabe.**
|
||||
vor der Umsetzung entschieden werden müssen.
|
||||
|
||||
> **Stand 2026-08-23:** Der Rocket.Chat-Teil ist **umgesetzt** —
|
||||
> `src/ClawdDotNet.Tools.RocketChat`, im `AppHost` registriert, `send_file`
|
||||
> freigabepflichtig. Der Befund unten bleibt als Begründung stehen; Abschnitt 2.0
|
||||
> hält fest, was die Messung gegen die echte Instanz an den Annahmen korrigiert hat.
|
||||
> **Offen:** der Nextcloud-Teil und die Kollision mit Roadmap A5 (Matrix) — die ist
|
||||
> eine Entscheidung, keine Umsetzung.
|
||||
|
||||
Verwandt: [Taskboard-Konzept](Taskboard-Konzept.md) (Scanner/Wake), [Staging-Konzept](Staging-Konzept.md)
|
||||
(Freigaben), [Audit-Konzept](Audit-Konzept.md), [Roadmap](Roadmap.md) (A5 — siehe Konflikt unten).
|
||||
@@ -177,7 +184,7 @@ der richtige Ansatz und wird von Rocket.Chat direkt unterstützt.
|
||||
- Authentifiziert wird jeder Aufruf über zwei Header: `X-Auth-Token` und `X-User-Id`.
|
||||
|
||||
**Entscheidung, die ich empfehle:** Das Anlegen der Benutzer ist **kein Agenten-Tool**.
|
||||
Es ist eine einmalige Einrichtungsfunktion in der WinForms-Oberfläche
|
||||
Es ist eine einmalige Einrichtungsfunktion in der Oberfläche
|
||||
(Instanz-Einstellungen → Rocket.Chat → „Agenten-Benutzer anlegen"). Sonst müsste ein
|
||||
Agent ein Admin-Token halten — und ein Admin-Token in Reichweite einer Prompt-Injection
|
||||
ist genau das, was A2 verhindern soll. Der Admin-Token liegt in der **Instanz**-Konfiguration,
|
||||
@@ -444,7 +451,7 @@ Das ist die Anforderung, die die Architektur bestimmt — nicht der Chat selbst.
|
||||
|
||||
| Kanal | Unabhängig von Rocket.Chat? | Richtung |
|
||||
|---|---|---|
|
||||
| WinForms-Chat (`frm_chat`) | vollständig — läuft in der App selbst | beide |
|
||||
| Chat-Seite in der App (`ChatPageView`) | vollständig — läuft in der App selbst | beide |
|
||||
| Telegram-Bot-Tool | ja — fremde Infrastruktur | beide |
|
||||
| Mail-Tool | ja, sofern der Mailserver anderswo läuft | beide |
|
||||
| Web-Chat / `ClawdDotNetApi` | ja, aber nur im lokalen Netz | beide |
|
||||
@@ -483,7 +490,7 @@ Ein Tool-Job `rocketchat_health` (Takt ~5 Minuten, `GET /api/info`):
|
||||
|
||||
**Eingehend während des Ausfalls:** Der Telegram-Poll-Job bleibt dauerhaft aktiv, nur mit
|
||||
langsamem Takt (z. B. alle 5 Minuten). Er kostet nichts, wenn nichts kommt — und ist im
|
||||
Ernstfall der Weg, auf dem *du* die Agenten erreichst. Der WinForms-Chat ist ohnehin immer
|
||||
Ernstfall der Weg, auf dem *du* die Agenten erreichst. Die eingebaute Chat-Seite ist ohnehin immer
|
||||
da, solange die App läuft.
|
||||
|
||||
**Entschieden (August 2026): Telegram ist der Notfallkanal.** Die Kanalliste lautet damit
|
||||
@@ -706,9 +713,9 @@ Damit der Zuschnitt klar ist:
|
||||
|
||||
- Keine Rocket.Chat-**App** (Apps-Engine, TypeScript im Server) — wir bleiben Client.
|
||||
- Keine Verwaltung von Rocket.Chat durch Agenten (Benutzer anlegen, Räume erstellen,
|
||||
Rechte vergeben). Das ist Admin-Arbeit in der WinForms-Oberfläche.
|
||||
Rechte vergeben). Das ist Admin-Arbeit in der Oberfläche.
|
||||
- Keine Sprach-/Videofunktionen, keine Nextcloud Talk-Anbindung.
|
||||
- Kein Ersatz für den WinForms-Chat — der bleibt und ist die unterste Rückfallebene.
|
||||
- Kein Ersatz für die eingebaute Chat-Seite — die bleibt und ist die unterste Rückfallebene.
|
||||
- Keine Ende-zu-Ende-Verschlüsselung. Rocket.Chat kann das, aber verschlüsselte Räume sind
|
||||
über die REST-API nicht lesbar. Agenten arbeiten in unverschlüsselten Räumen — das ist
|
||||
eine bewusste Einschränkung, die du kennen solltest.
|
||||
|
||||
@@ -102,7 +102,8 @@ Erst nach gewonnenem Übergang wird der eingefrorene Aufruf ausgeführt.
|
||||
## Offen
|
||||
|
||||
- **Review-Oberfläche** im Hauptfenster (Liste der offenen Vorschläge, Freigeben/Ablehnen)
|
||||
— die Dienst-API steht bereit; die WinForms-Ansicht ist die verbleibende Integration.
|
||||
— die Dienst-API steht bereit; die Freigabe-Ansicht in `src/ClawdDotNet.Desktop` ist
|
||||
die verbleibende Integration und der einzige offene Punkt von A2.
|
||||
- **Output-Scrubbing** greift auch hier auf den gespeicherten Argument-JSON, sobald es
|
||||
steht (eigener Roadmap-Punkt).
|
||||
- **Orders** (Handelsaufträge) reihen sich später als weitere `approve`-Aktionen ein.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Taskboard — Aufgaben statt Delay-Schleifen
|
||||
|
||||
> **Bauplan zu einem gebauten System.** Dateiformat, Wahrheitsaufteilung Datei/DB,
|
||||
> Scanner-Verhalten und Invarianten. Umgesetzt; der Stand steht in der
|
||||
> [Roadmap](Roadmap.md) 3.2. Die Abschnitte im Präsens beschreiben teils den Zustand
|
||||
> *vor* der Umsetzung — sie begründen den Entwurf.
|
||||
|
||||
Setzt A1 aus der [Roadmap](Roadmap.md) um. Das Taskboard ist das Fundament, auf dem
|
||||
Audit (A3), Staging (A2), Marktkalender (C1) und das Ergebnisregister (C7/C8)
|
||||
aufsetzen. Es löst zugleich sechs Altpunkte auf einmal (F-A5 Queue, F-A4 Run-Historie,
|
||||
@@ -15,8 +20,8 @@ Invarianten, Migration.
|
||||
Heute gibt es zwei getrennte, je für sich unzureichende Wege, einen Agenten Arbeit
|
||||
tun zu lassen:
|
||||
|
||||
- **Der Cron-Scheduler** ([`AgentScheduler`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs),
|
||||
[`ToolJobScheduler`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs)) hängt starr
|
||||
- **Der Cron-Scheduler** (`AgentScheduler`, `ToolJobScheduler` — beide inzwischen
|
||||
gelöscht, siehe unten) hing starr
|
||||
am Agenten: eine Cron-Zeile je Agent, ausgeführt über ein `Task.Delay` bis zum
|
||||
nächsten Termin. Ein jährlicher Termin bedeutet ein `Task.Delay` über Monate (B6).
|
||||
Cron läuft in Lokalzeit ohne explizite Zone (B7). Ob mit oder ohne Kontext gelaufen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ClawdDotNet — Teststrategie
|
||||
|
||||
Ergänzung zur [Bestandsaufnahme](Bestandsaufnahme-2026-07.md).
|
||||
Ergänzung zur [Bestandsaufnahme](archiv/Bestandsaufnahme-2026-07.md).
|
||||
Ziel: Fehlerklassen wie B1 (Compaction zerstört tool_call-Paarung) und B2 (Race Condition)
|
||||
strukturell unmöglich machen, statt sie im Betrieb zu entdecken.
|
||||
|
||||
@@ -61,9 +61,10 @@ internal sealed class FakeChatClient : IChatCompletionClient
|
||||
### R2 — `TimeProvider` statt `DateTime.Now`
|
||||
.NET 10 bringt `TimeProvider` mit; `FakeTimeProvider` steckt in
|
||||
`Microsoft.Extensions.TimeProvider.Testing`. Betrifft
|
||||
[`CronExpression.cs:53`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L53),
|
||||
[`AgentScheduler.cs:75`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L75),
|
||||
[`ToolJobScheduler.cs:111`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L111).
|
||||
[`CronExpression.cs:53`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L53) und den
|
||||
[`TaskScanner`](../src/ClawdDotNet.Core/Tasks/TaskScanner.cs). (Die ursprünglich hier
|
||||
genannten `AgentScheduler` und `ToolJobScheduler` sind gelöscht — der Scanner hat sie
|
||||
abgelöst.)
|
||||
|
||||
Damit werden Zeitumstellungs- und Langzeit-Tests deterministisch und laufen in Millisekunden
|
||||
statt in Echtzeit.
|
||||
@@ -116,7 +117,7 @@ tests/
|
||||
Die drei Projekte in `ClawdDotNet.slnx` unter einem Ordner `/tests/` eintragen.
|
||||
|
||||
> Nebenbefund: In `ClawdDotNet.slnx` fehlen vier Tool-Projekte (AgentComm, AgentSpawn,
|
||||
> AgentEditor, SocialMediaManager), obwohl sie in `ClawdDotNet.csproj` referenziert sind.
|
||||
> AgentEditor, SocialMediaManager), obwohl sie in `src/ClawdDotNet.App/ClawdDotNet.App.csproj` referenziert sind.
|
||||
> Sie sollten mit aufgenommen werden, sonst laufen sie in der IDE-Solution nicht mit.
|
||||
|
||||
---
|
||||
|
||||
@@ -21,7 +21,8 @@ ClawdDotNet.sln
|
||||
│ ├── ClawdDotNet.Tools.MeinTool/ ← Dein Tool-Plugin
|
||||
│ │ ├── ClawdDotNet.Tools.MeinTool.csproj
|
||||
│ │ └── MeinToolTool.cs
|
||||
│ └── ClawdDotNet.Host/ ← WinForms-App (verweist auf Core + alle Tools)
|
||||
│ ├── ClawdDotNet.App/ ← Fachschicht ohne Oberflaeche (verweist auf Core + alle Tools)
|
||||
│ └── ClawdDotNet.Desktop/ ← Oberflaeche in Avalonia (verweist auf App)
|
||||
```
|
||||
|
||||
### Neues Tool-Projekt anlegen
|
||||
@@ -450,7 +451,7 @@ Das Logging-System trennt automatisch nach Modul. Wenn dein Tool den Logger aus
|
||||
Logs/
|
||||
├── 2026-05-12/
|
||||
│ ├── Core.log ← Engine, Scheduler, Config
|
||||
│ ├── Host.log ← WinForms UI
|
||||
│ ├── Host.log ← Oberflaeche
|
||||
│ ├── Tool_Database.log ← Database-Tool
|
||||
│ ├── Tool_FileRW.log ← FileRW-Tool
|
||||
│ ├── Tool_MeinTool.log ← Dein Tool!
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
Vollständiges Review von Core-Engine, Security-Layer, Tools, Scheduling und UI.
|
||||
Stand: Commit `92e50d3`.
|
||||
|
||||
> **Dies ist ein datierter Befund, keine Todoliste.** Er wird nicht fortgeschrieben.
|
||||
> Was davon offen ist, steht in der [Roadmap](../Roadmap.md) — dort auch, was seither
|
||||
> erledigt wurde. Die Abschnitte zur Oberfläche beziehen sich auf die Windows-Forms-
|
||||
> Fassung, die es seit dem 2026-08-23 nicht mehr gibt.
|
||||
|
||||
**Kurzfassung:** Die Architektur ist gut — die Trennung Core/Tools/UI, das `IAgentTool`-Interface,
|
||||
der Tool-Job-Mechanismus und das Instanz-Konzept tragen. Die Probleme liegen fast alle in
|
||||
drei Bereichen: (1) Sicherheitsprüfungen, die als String-Vergleiche implementiert sind und
|
||||
@@ -14,7 +19,7 @@ deshalb umgehbar sind, (2) fehlende Nebenläufigkeits-Absicherung im geteilten C
|
||||
## 1. Kritische Sicherheitslücken (P0)
|
||||
|
||||
### S1 — DatabaseTool: Tabellen-Whitelist ist vollständig umgehbar
|
||||
[`DatabaseTool.cs:218`](../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L218)
|
||||
[`DatabaseTool.cs:218`](../../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L218)
|
||||
|
||||
```csharp
|
||||
return allowed.Any(t => t != null && inputLower.Contains(t));
|
||||
@@ -31,7 +36,7 @@ DELETE FROM users -- prices
|
||||
Die `users`-Tabelle wird gelöscht. Gleiches gilt für jede beliebige andere Tabelle.
|
||||
|
||||
Zusätzlich:
|
||||
- `IsWriteAttempt`/`IsAdminAttempt` ([Z.189/195](../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L189))
|
||||
- `IsWriteAttempt`/`IsAdminAttempt` ([Z.189/195](../../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L189))
|
||||
sind ebenfalls Substring-Prüfungen → False Positives (`SELECT * FROM prices WHERE note='update'`
|
||||
wird als Schreibzugriff blockiert).
|
||||
- Mehrere Statements pro Aufruf sind nicht unterbunden (`;`-Verkettung).
|
||||
@@ -43,7 +48,7 @@ vordefinierte, parametrisierte Named Queries ersetzen. Freies SQL vom LLM ist gr
|
||||
schwer abzusichern.
|
||||
|
||||
### S2 — SocialMediaManager: Kommando-Injection über yt-dlp
|
||||
[`SocialMediaManagerTool.cs:442`](../src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs#L442), auch Z.484 und Z.490
|
||||
[`SocialMediaManagerTool.cs:442`](../../src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs#L442), auch Z.484 und Z.490
|
||||
|
||||
```csharp
|
||||
var psi = new ProcessStartInfo(ytDlpPath, $"--print \"%(id)s\" --playlist-end 1 {channelUrl}")
|
||||
@@ -63,7 +68,7 @@ eine Regex validieren (`^https://(www\.)?(youtube\.com|youtu\.be)/…`) und `--`
|
||||
vor dem URL-Argument setzen.
|
||||
|
||||
### S3 — DirectAPI: API-Keys landen im Modell-Kontext
|
||||
[`DirectAPITool.cs:83`](../src/ClawdDotNet.Tools.DirectAPI/DirectAPITool.cs#L83), auch Z.99, 154, 177, 263
|
||||
[`DirectAPITool.cs:83`](../../src/ClawdDotNet.Tools.DirectAPI/DirectAPITool.cs#L83), auch Z.99, 154, 177, 263
|
||||
|
||||
```csharp
|
||||
var url = $"https://api.twelvedata.com/quote?symbol={symbol}&apikey={apiKey}";
|
||||
@@ -81,7 +86,7 @@ landet der Key:
|
||||
grundsätzlich eine bereinigte URL zurückgeben (Query-String entfernen oder `apikey` maskieren).
|
||||
|
||||
### S4 — PermissionGate ist faktisch wirkungslos
|
||||
[`PermissionGate.cs:7`](../src/ClawdDotNet.Core/Security/PermissionGate.cs#L7)
|
||||
[`PermissionGate.cs:7`](../../src/ClawdDotNet.Core/Security/PermissionGate.cs#L7)
|
||||
|
||||
```csharp
|
||||
public bool IsAllowed(string agentId, string toolName, AgentConfig agentConfig)
|
||||
@@ -98,13 +103,13 @@ Rate-Limits pro Agent/Tool/Zeitfenster, Audit-Log jedes Aufrufs mit Argumenten,
|
||||
Approval-Hook für irreversible Aktionen (siehe F-A1).
|
||||
|
||||
### S5 — WebFetch: kein SSRF-Schutz bei Redirects
|
||||
[`WebFetchTool.cs:58`](../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L58)
|
||||
[`WebFetchTool.cs:58`](../../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L58)
|
||||
|
||||
Die Domain-Whitelist wird nur auf die *ursprüngliche* URL angewendet. `HttpClient` folgt
|
||||
Redirects standardmäßig — eine erlaubte Domain kann auf `http://169.254.169.254/`,
|
||||
`http://localhost:8418/` (dein Gitea!) oder beliebige interne Hosts weiterleiten.
|
||||
|
||||
Nebenbei: `uri.Host.Replace("www.", "")` ([Z.59](../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L59))
|
||||
Nebenbei: `uri.Host.Replace("www.", "")` ([Z.59](../../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L59))
|
||||
ersetzt das Fragment überall im Hostnamen, nicht nur am Anfang.
|
||||
|
||||
**Fix:** `HttpClientHandler { AllowAutoRedirect = false }` und Redirects manuell auflösen,
|
||||
@@ -112,7 +117,7 @@ dabei jede Zwischen-URL erneut gegen die Whitelist prüfen. Zusätzlich private
|
||||
(RFC1918, Loopback, Link-Local) hart blockieren.
|
||||
|
||||
### S6 — FileRW: Path-Traversal-Prüfung per Präfix
|
||||
[`FileRWTool.cs:211`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L211)
|
||||
[`FileRWTool.cs:211`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L211)
|
||||
|
||||
```csharp
|
||||
if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -128,7 +133,7 @@ ausnutzbar, aber eine Zeitbombe.
|
||||
### S7 — Secrets im Klartext
|
||||
`openRouterApiKey`, DB-`connectionString` (mit Passwort), Mail-`password`, Telegram-`password2FA`
|
||||
liegen unverschlüsselt in `AgentSettings.json` / `InstanceConfig.json`
|
||||
([`InstanceConfig.cs:46`](../src/ClawdDotNet.Core/Config/InstanceConfig.cs#L46)).
|
||||
([`InstanceConfig.cs:46`](../../src/ClawdDotNet.Core/Config/InstanceConfig.cs#L46)).
|
||||
|
||||
**Fix:** DPAPI (`ProtectedData.Protect` mit `CurrentUser`-Scope) für alle Secret-Felder,
|
||||
oder Windows Credential Manager. Zumindest sollten die Felder beim Speichern verschlüsselt
|
||||
@@ -139,7 +144,7 @@ und erst zur Laufzeit entschlüsselt werden.
|
||||
## 2. Bugs (P1)
|
||||
|
||||
### B1 — ContextCompactor zerstört die tool_call-Paarung ⚠️
|
||||
[`ContextCompactor.cs:140`](../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
|
||||
[`ContextCompactor.cs:140`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
|
||||
|
||||
```csharp
|
||||
var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
|
||||
@@ -161,7 +166,7 @@ einer Nachricht mit Rolle `user` oder `assistant` ohne `tool_calls`. Analog muss
|
||||
`assistant` mit `tool_calls` am Ende immer seine vollständigen `tool`-Antworten behalten.
|
||||
|
||||
### B2 — Race Condition auf dem geteilten Chat-Kontext ⚠️
|
||||
[`AgentEngine.cs:250`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L250) und Z.268/316/322
|
||||
[`AgentEngine.cs:250`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L250) und Z.268/316/322
|
||||
|
||||
`_chatContexts[agentId]` ist eine geteilte `List<ChatMessage>`. Nur der *Lookup* läuft unter
|
||||
`_lock` — alle `messages.Add(...)` im Loop passieren ungeschützt.
|
||||
@@ -185,8 +190,8 @@ Beim Umsetzen kamen zwei Folgeprobleme dazu, die denselben Ursprung haben:
|
||||
(der laufende Chat hält es bereits) — wird jetzt abgelehnt.
|
||||
|
||||
### B3 — `maxTokens` vermischt Abrechnungs-Budget und Kontextgröße ⚠️
|
||||
[`LoopGuard.cs:22`](../src/ClawdDotNet.Core/Engine/LoopGuard.cs#L22), Defaults in
|
||||
[`AgentConfig.cs:105`](../src/ClawdDotNet.Core/Config/AgentConfig.cs#L105)
|
||||
[`LoopGuard.cs:22`](../../src/ClawdDotNet.Core/Engine/LoopGuard.cs#L22), Defaults in
|
||||
[`AgentConfig.cs:105`](../../src/ClawdDotNet.Core/Config/AgentConfig.cs#L105)
|
||||
|
||||
`RecordTokens` summiert `response.Usage.TotalTokens` über alle Schritte. Da jeder Schritt den
|
||||
**kompletten** Kontext erneut sendet, wächst diese Summe quadratisch:
|
||||
@@ -209,7 +214,7 @@ häufigste Frustquelle im laufenden Betrieb.
|
||||
und beide getrennt im UI ausweisen.
|
||||
|
||||
### B4 — Kostenanzeige ist doppelt falsch
|
||||
[`frm_main.cs:292`](../frm_main.cs#L292)
|
||||
[`frm_main.cs:292`](../../frm_main.cs#L292)
|
||||
|
||||
```csharp
|
||||
_statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2);
|
||||
@@ -219,7 +224,7 @@ _statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2)
|
||||
Da Output ~5x teurer ist, überschätzt die Anzeige die Kosten um Faktor ~2,5.
|
||||
2. **Modell fehlt in der Preistabelle**: `AgentConfig.Model` hat den Default
|
||||
`anthropic/claude-sonnet-4-5` — dieser Eintrag existiert in `ModelPricing`
|
||||
([`OpenRouterStatusService.cs:15`](../Services/OpenRouterStatusService.cs#L15)) nicht.
|
||||
([`OpenRouterStatusService.cs:15`](../../Services/OpenRouterStatusService.cs#L15)) nicht.
|
||||
`CalculateCost` gibt dann stillschweigend `0` zurück.
|
||||
|
||||
Die Tabelle ist zudem veraltet (`claude-sonnet-4`, `claude-opus-4`, `claude-haiku-4.5`).
|
||||
@@ -230,22 +235,22 @@ statt hartzucodieren. Bei unbekanntem Modell sichtbar warnen statt 0 anzuzeigen.
|
||||
|
||||
### B5 — Tool-Ergebnisse landen ungekappt im Kontext
|
||||
Ein einziger `WebFetch` mit dem Default `maxResponseKb: 512`
|
||||
([`WebFetchTool.cs:90`](../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L90)) erzeugt bis zu
|
||||
([`WebFetchTool.cs:90`](../../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L90)) erzeugt bis zu
|
||||
512 KB Text ≈ **130.000 Tokens** in einer einzigen Tool-Antwort. `FileRW.read`
|
||||
([`FileRWTool.cs:245`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L245)) hat gar kein Limit,
|
||||
([`FileRWTool.cs:245`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L245)) hat gar kein Limit,
|
||||
`Database.query` und `Mail.read_inbox` ebenfalls nicht.
|
||||
|
||||
Gekürzt wird erst nachträglich in der Compaction — und dort nur außerhalb der letzten
|
||||
6 Nachrichten. Der teure Request ist zu dem Zeitpunkt längst bezahlt.
|
||||
|
||||
**Fix:** Kappung beim Einfügen in `ExecuteToolCallAsync`
|
||||
([`AgentEngine.cs:668`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L668)) — zentral, für alle
|
||||
([`AgentEngine.cs:668`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L668)) — zentral, für alle
|
||||
Tools, mit konfigurierbarem `maxToolResultTokens` und einem Hinweis an das Modell, dass gekürzt
|
||||
wurde (inkl. Angebot, gezielt nachzulesen).
|
||||
|
||||
### B6 — `Task.Delay` wirft bei langen Cron-Intervallen
|
||||
[`AgentScheduler.cs:89`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L89) und
|
||||
[`ToolJobScheduler.cs:126`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L126)
|
||||
[`AgentScheduler.cs:89`](../../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L89) und
|
||||
[`ToolJobScheduler.cs:126`](../../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L126)
|
||||
|
||||
`Task.Delay` wirft `ArgumentOutOfRangeException` bei Werten über ~24,8 Tagen. Ein jährlicher
|
||||
Cron (`0 3 1 1 *`) erzeugt eine Wartezeit von bis zu 365 Tagen. Der `catch` fängt nur
|
||||
@@ -257,14 +262,14 @@ Job läuft ab dann einfach nie wieder.
|
||||
Scheduler-Tasks mit einem `ContinueWith`-Fehler-Logger versehen.
|
||||
|
||||
### B7 — Cron rechnet in Lokalzeit
|
||||
[`CronExpression.cs:51`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L51) mit
|
||||
[`CronExpression.cs:51`](../../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L51) mit
|
||||
`DateTime.Now`. Bei Zeitumstellung: im Oktober läuft ein `0 2 * * *`-Job doppelt, im März gar nicht.
|
||||
|
||||
**Fix:** Intern in UTC rechnen und nur für die Anzeige konvertieren, oder `TimeZoneInfo`
|
||||
explizit berücksichtigen.
|
||||
|
||||
### B8 — Keine Rekursionsbremse bei AgentComm / AgentSpawn
|
||||
[`AgentEngine.cs:510`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L510)
|
||||
[`AgentEngine.cs:510`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L510)
|
||||
|
||||
Agent A ruft `send_message(B)` → `ChatAsync(B)` → B ruft `send_message(A)` → … Es gibt keine
|
||||
Tiefenbegrenzung und keinen Zyklus-Check. Bei `SpawnAgentAsync` schützt immerhin der
|
||||
@@ -275,14 +280,14 @@ in der Zwischenzeit brennt jeder Hop einen vollständigen LLM-Run.
|
||||
den Aufrufpfad zur Zyklenerkennung mitgeben.
|
||||
|
||||
### B9 — `index_Count` liest die Datei außerhalb des Locks erneut
|
||||
[`FileRWTool.cs:569`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L569) ruft
|
||||
[`index_Count`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L572) auf, nachdem das Lock
|
||||
[`FileRWTool.cs:569`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L569) ruft
|
||||
[`index_Count`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L572) auf, nachdem das Lock
|
||||
freigegeben wurde — Race mit parallelen `stock_add`-Aufrufen, plus ein überflüssiges
|
||||
vollständiges Parsen der Index-Datei. Der Zähler ist innerhalb des Locks ohnehin bekannt
|
||||
(`index.Count`).
|
||||
|
||||
### B10 — `PersistChatState` schreibt bei jedem Eintrag alles neu
|
||||
[`AgentEngine.cs:593`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L593)
|
||||
[`AgentEngine.cs:593`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L593)
|
||||
|
||||
Vollständige History **und** vollständiger Kontext werden als `WriteIndented`-JSON bei jedem
|
||||
einzelnen Chat-Eintrag rausgeschrieben → O(n²) Schreiblast über eine Sitzung. Bei einem Agenten
|
||||
@@ -292,19 +297,19 @@ mit 500 Nachrichten sind das mehrere MB pro Nachricht.
|
||||
`WriteIndented = false`.
|
||||
|
||||
### B11 — Kein `max_tokens` im Request
|
||||
[`AgentEngine.cs:105`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L105) und Z.279 setzen
|
||||
[`AgentEngine.cs:105`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L105) und Z.279 setzen
|
||||
`ChatRequest.MaxTokens` nie. Ein Modell kann in einem Schritt sein volles Output-Limit
|
||||
ausschöpfen.
|
||||
|
||||
### B12 — Kein Retry/Backoff
|
||||
[`OpenRouterClient.cs:42`](../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L42) wirft bei jedem
|
||||
[`OpenRouterClient.cs:42`](../../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L42) wirft bei jedem
|
||||
Nicht-2xx sofort. Ein einzelnes HTTP 429 killt einen kompletten geplanten Run. Bei OpenRouter
|
||||
sind 429/502/503 im Normalbetrieb zu erwarten.
|
||||
|
||||
**Fix:** Polly o.ä. mit exponentiellem Backoff + Jitter für 429/5xx, `Retry-After` respektieren.
|
||||
|
||||
### B14 — Compaction dupliziert den System-Prompt bei kurzen Konversationen ⚠️
|
||||
[`ContextCompactor.cs:140`](../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
|
||||
[`ContextCompactor.cs:140`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
|
||||
|
||||
*Gefunden durch den Property-Test, nicht beim Lesen des Codes.*
|
||||
|
||||
@@ -341,7 +346,7 @@ verwenden dagegen das Feld `_instanceId`, das nur gesetzt wird, wenn
|
||||
|
||||
### K1 — Geplante Agenten haben kein Gedächtnis
|
||||
`RunAsync` baut bei jedem Cron-Tick eine frische Nachrichtenliste
|
||||
([`AgentEngine.cs:89`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L89)). Ein Agent, der alle
|
||||
([`AgentEngine.cs:89`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L89)). Ein Agent, der alle
|
||||
30 Minuten läuft, weiß nichts von seinem letzten Lauf: er ruft dieselben Quellen erneut ab,
|
||||
zieht dieselben Schlüsse und kann keine Entwicklung über Zeit verfolgen.
|
||||
|
||||
@@ -370,7 +375,7 @@ Erste Kandidaten: `CronExpression`, `ContextCompactor` (Paarungs-Invarianten!),
|
||||
`FileRWTool`-Pfadprüfungen.
|
||||
|
||||
### K4 — Kein Streaming
|
||||
`request.Stream = false` fest verdrahtet ([`OpenRouterClient.cs:34`](../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L34)).
|
||||
`request.Stream = false` fest verdrahtet ([`OpenRouterClient.cs:34`](../../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L34)).
|
||||
Bei langen Antworten wirkt die UI eingefroren; es gibt nur den Typing-Indicator.
|
||||
|
||||
### K5 — Kein Kostenlimit
|
||||
@@ -405,7 +410,7 @@ Input-Tokens kosten nur ~10 % des Normalpreises (Schreiben in den Cache kostet e
|
||||
Umsetzung:
|
||||
1. `ChatMessage.Content` muss das Array-Format unterstützen
|
||||
(`[{ "type": "text", "text": "...", "cache_control": { "type": "ephemeral" } }]`).
|
||||
Aktuell ist es ein reiner `string` ([`ChatMessage.cs:12`](../src/ClawdDotNet.Core/Api/Models/ChatMessage.cs#L12)).
|
||||
Aktuell ist es ein reiner `string` ([`ChatMessage.cs:12`](../../src/ClawdDotNet.Core/Api/Models/ChatMessage.cs#L12)).
|
||||
2. Cache-Breakpoint ans Ende des System-Prompts und ans Ende der Tool-Definitionen setzen.
|
||||
3. Optional einen dritten Breakpoint nach der letzten stabilen Konversationsgrenze (rollierend).
|
||||
4. `Usage` um `prompt_tokens_details.cached_tokens` erweitern, damit der Effekt messbar wird.
|
||||
@@ -421,7 +426,7 @@ Sinnvolle Defaults: 4.000 Tokens pro Tool-Ergebnis, mit Kürzungshinweis und der
|
||||
gezielt weiterzulesen (Offset-Parameter bei `FileRW.read`, `LIMIT`/`OFFSET` bei `Database.query`).
|
||||
|
||||
### T3 — Günstiges Modell für die Compaction (Einsparung: ~95 % der Compaction-Kosten) ★★
|
||||
[`ContextCompactor.cs:114`](../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L114) nutzt
|
||||
[`ContextCompactor.cs:114`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L114) nutzt
|
||||
`model` — also das teure Modell des Agenten — um bis zu 30k Zeichen zusammenzufassen.
|
||||
Bei Opus kostet eine einzige Compaction so mehr als der halbe Run.
|
||||
|
||||
@@ -430,7 +435,7 @@ Bei Opus kostet eine einzige Compaction so mehr als der halbe Run.
|
||||
|
||||
### T4 — Proaktiv statt reaktiv kompaktieren ★★
|
||||
`CompactIfNeededAsync` läuft **nach** dem API-Call
|
||||
([`AgentEngine.cs:123`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L123)) und nutzt die
|
||||
([`AgentEngine.cs:123`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L123)) und nutzt die
|
||||
`promptTokens` der gerade bezahlten Anfrage. Der überfüllte Prompt wurde also bereits berechnet.
|
||||
|
||||
**Fix:** Vor dem Senden prüfen (mit `EstimateTokens`, das es schon gibt) und erst dann den
|
||||
@@ -443,7 +448,7 @@ was die Tokenkosten verdoppelt statt sie zu begrenzen.
|
||||
### T6 — Tool-Definitionen verschlanken ★
|
||||
Die Beschreibungen sind großzügig: `FileRW` allein hat ~500 Zeichen Description plus ein
|
||||
Schema mit 11 Properties ≈ 900 Tokens
|
||||
([`FileRWTool.cs:26`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L26)). Bei 8 zugewiesenen
|
||||
([`FileRWTool.cs:26`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L26)). Bei 8 zugewiesenen
|
||||
Tools sind das schnell 4–6k Tokens — bei **jedem** Schritt (ohne Caching).
|
||||
|
||||
Maßnahmen:
|
||||
@@ -453,7 +458,7 @@ Maßnahmen:
|
||||
- Für Agenten mit vielen Tools: zweistufiges Laden (`list_toolsets` → `load_toolset`).
|
||||
|
||||
### T7 — `RunAsync` statt `ChatAsync` für Job-Wakeups prüfen ★
|
||||
[`ToolJobScheduler.cs:186`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L186)
|
||||
[`ToolJobScheduler.cs:186`](../../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L186)
|
||||
entscheidet über `UseChatContext`. Mit `true` wird die komplette (potenziell riesige)
|
||||
Chat-Historie in einen "Prüfe neue Mails"-Tick gezogen. Für zustandslose Ticks ist `RunAsync`
|
||||
um Größenordnungen günstiger — der Default sollte bewusst gesetzt und im UI erklärt sein.
|
||||
@@ -504,7 +509,7 @@ und im UI markiert. Setzt B4 (korrekte Kostenerfassung) voraus.
|
||||
|
||||
**F-A4 — Run-Historie**
|
||||
`AgentRunResult` wird aktuell nur im Speicher als "letztes Ergebnis" gehalten
|
||||
([`AgentScheduler.cs:14`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L14)).
|
||||
([`AgentScheduler.cs:14`](../../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L14)).
|
||||
Persistierte Runs (mit Schritten, Tokens, Kosten, Fehlern) wären die Grundlage für Diagnose
|
||||
und Kostenanalyse.
|
||||
|
||||
@@ -523,7 +528,7 @@ Siehe K3.
|
||||
|
||||
## 6. Vorgeschlagene Reihenfolge
|
||||
|
||||
> **Abgelöst durch die [Roadmap](Roadmap.md)** (Juli 2026). Die offenen Punkte
|
||||
> **Abgelöst durch die [Roadmap](../Roadmap.md)** (Juli 2026). Die offenen Punkte
|
||||
> werden dort weitergeführt; dieser Abschnitt bleibt als Stand der Bestandsaufnahme
|
||||
> eingefroren. F-A1/S4, F-A2, F-A5, T6, T7 sowie B6–B8 sind in den Roadmap-Vorhaben
|
||||
> A1–A4 aufgegangen.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Deploymentcenter 2.2 – 2.4: Was noch zu tun ist
|
||||
|
||||
Stand: 2026-08-13. Ergänzt [Deploymentcenter-Integration](Deploymentcenter-Integration.md)
|
||||
Stand: 2026-08-13. Ergänzt [Deploymentcenter-Integration](../Deploymentcenter-Integration.md)
|
||||
(dort steht der Stand nach 2.1) um die drei neuen Ausbaustufen.
|
||||
|
||||
| Fassung | Was dazukam | Betrifft uns |
|
||||
@@ -40,7 +40,7 @@ pack-and-deploy --config deploy/packager.config.json \
|
||||
```
|
||||
|
||||
- Zugangsdaten in `deploy/packager.config.json` (per `.gitignore` ausgeschlossen),
|
||||
Vorlage ohne Werte in [`packager.config.example.json`](../deploy/packager.config.example.json).
|
||||
Vorlage ohne Werte in [`packager.config.example.json`](../../deploy/packager.config.example.json).
|
||||
- **`ftpRemoteBaseDir` ist `/releases`**, nicht `/public_html/releases` wie in der
|
||||
Packager-Vorlage: Auf diesem Server liegt die Release-Ablage auf der FTP-Wurzel.
|
||||
- Der Packager veröffentlicht mit einem **Sub-Token**, das nur `updateservice:publish`
|
||||
@@ -50,7 +50,7 @@ pack-and-deploy --config deploy/packager.config.json \
|
||||
Deploymentcenter-Projektbaum in die FTP-Wurzel und hat mit dem Veröffentlichen eines
|
||||
Anwendungspakets nichts zu tun.
|
||||
- Die Versionsgegenprobe des Packagers greift und passt: `<Version>` aus
|
||||
[Directory.Build.props](../Directory.Build.props) stimmt mit `clawddotnet.dll` überein.
|
||||
[Directory.Build.props](../../Directory.Build.props) stimmt mit `clawddotnet.dll` überein.
|
||||
|
||||
Offen: `linux-x64` (erst nach der Avalonia-Portierung) und `prod`.
|
||||
|
||||
@@ -63,7 +63,7 @@ Unsere Konfiguration liegt seit der Linux-Portierung in `AppPaths.ConfigDirector
|
||||
(`%APPDATA%` bzw. XDG), **nicht** neben der Programmdatei. Ein Update kann sie also gar
|
||||
nicht überschreiben. Zu prüfen bleibt nur, dass keine leeren Arbeitsordner ins Paket
|
||||
wandern — die `CreateWorkingDirectories`-Targets in
|
||||
[ClawdDotNet.csproj](../ClawdDotNet.csproj) legen `tools/`, `Logs/` und `Instances/`
|
||||
[ClawdDotNet.csproj](../../ClawdDotNet.csproj) legen `tools/`, `Logs/` und `Instances/`
|
||||
unter `OutputPath` an, und die sind mit AppPaths ohnehin überholt.
|
||||
|
||||
---
|
||||
@@ -72,7 +72,7 @@ unter `OutputPath` an, und die sind mit AppPaths ohnehin überholt.
|
||||
|
||||
Aus dem Hinweis ist eine Rückfrage geworden („Jetzt installieren" / „Später"), die den
|
||||
`update-agent` startet. Umgesetzt in
|
||||
[`DeploymentcenterService.StartUpdate`](../src/ClawdDotNet.App/Services/DeploymentcenterService.cs)
|
||||
[`DeploymentcenterService.StartUpdate`](../../src/ClawdDotNet.App/Services/DeploymentcenterService.cs)
|
||||
und `App.StartUpdateAsync`.
|
||||
|
||||
### Der Agent wird mitgeliefert — er muss es
|
||||
@@ -82,7 +82,7 @@ wo der Benutzer sie hingelegt hat. `ResolveAgentPath()` sucht ihn aber neben der
|
||||
Anwendung. Ohne Mitliefern fände die Anwendung nie einen Agenten und könnte sich nicht
|
||||
aktualisieren.
|
||||
|
||||
[`deploy/publish.py`](../deploy/publish.py) holt das ausgelieferte Binary von
|
||||
[`deploy/publish.py`](../../deploy/publish.py) holt das ausgelieferte Binary von
|
||||
`/installer/`, **prüft die SHA256 gegen `installer.json`** und legt es plattformrichtig
|
||||
ab (`update-agent.exe` bzw. `update-agent`). Bewusst das offizielle statt eines selbst
|
||||
gebauten: Es ist dasselbe, das die Erstinstallation verwendet, und wird zentral gepflegt.
|
||||
@@ -126,7 +126,7 @@ schreibbar ist ([Linux-Analyse](Linux-Portierung-Analyse.md)).
|
||||
|
||||
Das Deploymentcenter hat daraufhin `location` am Ziel ergänzt (`install`, `config`,
|
||||
`data`, `home`) samt Variablenersetzung in `file`. Damit ist der Weg frei;
|
||||
[`setup.json`](../src/ClawdDotNet.Desktop/setup.json) liegt im Projekt und wird ins
|
||||
[`setup.json`](../../src/ClawdDotNet.Desktop/setup.json) liegt im Projekt und wird ins
|
||||
Ausgabeverzeichnis kopiert, landet also im Paket neben der `manifest.json`.
|
||||
|
||||
### Der Ordnername ist bewusst kleingeschrieben
|
||||
@@ -148,7 +148,7 @@ leeres Verzeichnis neben dem, aus dem die Anwendung liest.
|
||||
Das Token stellt der Server aus (`source: "provision"`), die Server-Adresse kommt aus dem
|
||||
Installer (`detect:baseurl`). Damit entfällt der Absatz „bis die Avalonia-Einstellungs-
|
||||
ansicht steht, von Hand in `Settings.json`" aus der
|
||||
[Integrationsbeschreibung](Deploymentcenter-Integration.md) — jedenfalls für frisch
|
||||
[Integrationsbeschreibung](../Deploymentcenter-Integration.md) — jedenfalls für frisch
|
||||
installierte Systeme.
|
||||
|
||||
Der Installer schreibt Lizenzschlüssel und Token **im Klartext**; er kennt unsere
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **Nachtrag 2026-08-08 — die Anbindung ist umgestellt, Server und SDK stehen auf 2.1.**
|
||||
> Abschnitt 4 und 5 sind abgearbeitet; wie es jetzt aussieht, steht in
|
||||
> [Deploymentcenter-Integration](Deploymentcenter-Integration.md).
|
||||
> [Deploymentcenter-Integration](../Deploymentcenter-Integration.md).
|
||||
>
|
||||
> Mit **SDK 2.1 erledigt** (waren Befunde aus Abschnitt 3 bzw. aus der Durchsicht der
|
||||
> 2.0-Anbindung):
|
||||
@@ -48,7 +48,7 @@ Punkte aus derselben Durchsicht — drei davon würden beim Ausrollen wehtun.
|
||||
|
||||
| Punkt aus dem Vorschlag | Umsetzung |
|
||||
|---|---|
|
||||
| Format `2:<plattform>:<hex>` | [HardwareId.cs:125](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs) |
|
||||
| Format `2:<plattform>:<hex>` | [HardwareId.cs:125](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs) |
|
||||
| **Kein `MachineName` im Hash** | `ComputeV2Hash`, `:138` — der wichtigste Punkt, sauber umgesetzt |
|
||||
| Quellenkette Windows/Linux | `:42–107`, inklusive `dmi-uuid` |
|
||||
| `IsPlausibleMachineId` (Länge, `uninitialized`, nur Nullen) | `:161` |
|
||||
@@ -57,11 +57,11 @@ Punkte aus derselben Durchsicht — drei davon würden beim Ausrollen wehtun.
|
||||
| Erweiterte Stoppwortliste | `:23` — inkl. `br-`, `virbr`, `cni`, `cali` |
|
||||
| `machine.key` mit `0600` | `:290`, `SetUnixPermissions` mit `#if NET8_0_OR_GREATER` |
|
||||
| Vorgabe per Umgebungsvariable | `LicenseConfig.HardwareIdOverride`, beide Namen |
|
||||
| XDG-Auflösungskette, nie leerer Pfad | [LicenseConfig.cs:28](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseConfig.cs), mit `ValidateNonEmpty` |
|
||||
| XDG-Auflösungskette, nie leerer Pfad | [LicenseConfig.cs:28](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseConfig.cs), mit `ValidateNonEmpty` |
|
||||
| Mehrfachziel `netstandard2.0;net8.0` | csproj, BouncyCastle nur im netstandard-Zweig |
|
||||
| `LLS2`-Hülle, AES-GCM, HKDF | [StateStore.cs:169](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) — Schlüssel aus HW-ID abgeleitet, bindet den Cache also echt an die Maschine |
|
||||
| `LLS2`-Hülle, AES-GCM, HKDF | [StateStore.cs:169](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) — Schlüssel aus HW-ID abgeleitet, bindet den Cache also echt an die Maschine |
|
||||
| `ILicensePrompt` + Konsolenfassung | vorhanden — genau das, was der kopflose Host braucht |
|
||||
| Servermigration v1→v2 | [LicenseService.php:108](../../Deploymentcenter/src/Modules/License/LicenseService.php), mit Prüfprotokolleintrag `hwid_migrated` |
|
||||
| Servermigration v1→v2 | [LicenseService.php:108](../../../Deploymentcenter/src/Modules/License/LicenseService.php), mit Prüfprotokolleintrag `hwid_migrated` |
|
||||
| Schema `hwid_version`/`hwid_source`/`platform` | `sql/migrations/v2_hardware_id.sql`, rückwärtskompatibel |
|
||||
| Verwaltungsansicht zeigt Quelle/Plattform | `public/index.php:1227` |
|
||||
|
||||
@@ -80,7 +80,7 @@ das ist jetzt reine Anschlussarbeit in ClawdDotNet, keine Konzeptarbeit mehr.
|
||||
|
||||
### 2.1 Es wird nichts signiert — die Lizenzprüfung ist eine Vertrauensfrage an DNS
|
||||
|
||||
[LicenseClient.cs:62](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs):
|
||||
[LicenseClient.cs:62](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs):
|
||||
|
||||
```csharp
|
||||
string status = root.TryGetProperty("status", out var sProp) ? sProp.GetString() ?? "unknown" : "unknown";
|
||||
@@ -135,7 +135,7 @@ Der Migrationsweg ist auf beiden Seiten korrekt gebaut — er wird nur nie
|
||||
auslösen, weil der Client eine andere v1-ID berechnet als die, die in der
|
||||
Datenbank steht.
|
||||
|
||||
Alt ([LicenseLabrador/HardwareId.cs:20](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)):
|
||||
Alt ([LicenseLabrador/HardwareId.cs:20](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)):
|
||||
|
||||
```csharp
|
||||
rawBuilder.Append(machineId); // MachineGuid, sonst MAC
|
||||
@@ -143,7 +143,7 @@ rawBuilder.Append(Environment.MachineName); // direkt angehängt, kein Tren
|
||||
→ sha256(machineGuid + machineName)
|
||||
```
|
||||
|
||||
Neu ([HardwareId.cs:149](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs)):
|
||||
Neu ([HardwareId.cs:149](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs)):
|
||||
|
||||
```csharp
|
||||
string raw = $"{Environment.MachineName}:{firstMac}";
|
||||
@@ -174,7 +174,7 @@ Aktivierungsplätze schon verbraucht sind.
|
||||
|
||||
### 2.3 Der Klartext-Rückfall ist noch da, nur woanders
|
||||
|
||||
[StateStore.cs:79](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) —
|
||||
[StateStore.cs:79](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) —
|
||||
„Legacy Migration Check":
|
||||
|
||||
```csharp
|
||||
@@ -217,7 +217,7 @@ und ist als Cache wertlos.
|
||||
nichts. Alte Cachedateien sollen verworfen werden — eine einmalige
|
||||
Online-Prüfung ist der ganze Preis.
|
||||
|
||||
Nebenbei: `Checksum = hwInfo.HardwareId` ([LicenseClient.cs:79](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs))
|
||||
Nebenbei: `Checksum = hwInfo.HardwareId` ([LicenseClient.cs:79](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs))
|
||||
ist keine Prüfsumme, sondern eine Kopie der HW-ID. Das Feld ist damit ohne
|
||||
Funktion — entweder mit einem HMAC über die übrigen Felder füllen oder entfernen,
|
||||
damit niemand später Schutz vermutet, wo keiner ist.
|
||||
@@ -228,7 +228,7 @@ damit niemand später Schutz vermutet, wo keiner ist.
|
||||
|
||||
| Fundstelle | Sache |
|
||||
|---|---|
|
||||
| [LicenseClient.cs:26](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs) | Eigener `HttpClient` je Instanz, nie freigegeben, **ohne Zeitgrenze** (Vorgabe 100 s). Der alte `LicenseConfig.HttpTimeout` war 6 s. In `LicenseGate.RunStartupCheck` bedeutet das bis zu 100 s Standbild beim Start, wenn der Server nicht antwortet. |
|
||||
| [LicenseClient.cs:26](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs) | Eigener `HttpClient` je Instanz, nie freigegeben, **ohne Zeitgrenze** (Vorgabe 100 s). Der alte `LicenseConfig.HttpTimeout` war 6 s. In `LicenseGate.RunStartupCheck` bedeutet das bis zu 100 s Standbild beim Start, wenn der Server nicht antwortet. |
|
||||
| `:107` | `catch (Exception ex)` um den gesamten Block: Auch ein Fehler beim Auswerten einer *erfolgreichen* Antwort landet im Offline-Zweig. Ein defekter Server gilt dann als „offline". |
|
||||
| `:47` | `app_version = "1.0.0"` fest verdrahtet. ClawdDotNet hat `BuildInfo.Build` — sollte Parameter sein, sonst steht in der Verwaltungsansicht bei jeder Instanz dasselbe. |
|
||||
| `:32`, `:172` | `HardwareId.GetHardwareId()` bei jedem Aufruf neu: liest unter Linux Dateien und zählt Netzwerkschnittstellen auf. Einmal berechnen und halten. |
|
||||
@@ -277,7 +277,7 @@ abzugleichen.
|
||||
| [Program.cs:112](Program.cs:112) | Lizenzprüfung so verlagern, dass sie ohne Fenster auskommt (kopfloser Host). |
|
||||
| Host (neu) | `--license-status`, `--license-set-key`, `--license-deactivate` — der Client bringt alles Nötige mit. |
|
||||
| [WatchdogClient.cs](src/ClawdDotNet.Core/Watchdog/WatchdogClient.cs) | Pfade auf `/api/watchdog/v1/…`; Registrierungsweg klären (Abschnitt 4). |
|
||||
| `docs/Integrationsplan-WatchDog-LicenseLabrador.md` | Abgelöst durch [Deploymentcenter-Integration](Deploymentcenter-Integration.md). |
|
||||
| `docs/Integrationsplan-WatchDog-LicenseLabrador.md` | Abgelöst durch [Deploymentcenter-Integration](../Deploymentcenter-Integration.md). |
|
||||
|
||||
---
|
||||
|
||||
@@ -57,8 +57,8 @@ Abhängigkeit.
|
||||
## Problem 3: JSON-Dateien werden nicht atomar geschrieben ⚠️
|
||||
|
||||
Alle Schreibvorgänge laufen über `File.WriteAllText`
|
||||
([InstanceDirectoryManager.cs:439](../Services/InstanceDirectoryManager.cs#L439),
|
||||
[AgentEngine.cs:783](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L783)). Ein Absturz
|
||||
([InstanceDirectoryManager.cs:439](../../Services/InstanceDirectoryManager.cs#L439),
|
||||
[AgentEngine.cs:783](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L783)). Ein Absturz
|
||||
oder Stromausfall mitten im Schreiben hinterlässt eine abgeschnittene Datei.
|
||||
|
||||
**Das ist bereits passiert:** In der Instanz `TradingTeam` liegt eine
|
||||
@@ -278,7 +278,7 @@ Rohzahlen.
|
||||
|
||||
# Vorgeschlagene Reihenfolge
|
||||
|
||||
> **Abgelöst durch die [Roadmap](Roadmap.md)** (Juli 2026). Die offenen Punkte
|
||||
> **Abgelöst durch die [Roadmap](../Roadmap.md)** (Juli 2026). Die offenen Punkte
|
||||
> laufen dort als C1–C8 weiter; S4 + K2 sind in den Vorhaben A2/A3
|
||||
> (Staging-Freigabe, Audit-Log) aufgegangen.
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
Stand: 2026-08-06. Reine Bestandsaufnahme und Aufwandsschätzung, **kein** Umbau.
|
||||
|
||||
> **Nachtrag 2026-08-23:** Der teure Teil dieser Analyse hat sich erledigt. Die rund
|
||||
> 8.900 Zeilen Windows-Forms-Oberfläche samt WebView2 und den vier `PropertyGrid`-
|
||||
> Instanzen gibt es nicht mehr — sie sind durch `src/ClawdDotNet.Desktop` (Avalonia,
|
||||
> `net10.0`) ersetzt und beim Frühjahrsputz entfernt worden. Damit fällt der zweite
|
||||
> der beiden unten vorgeschlagenen Schnitte weg: Es gibt keine Windows-gebundene
|
||||
> Oberfläche mehr, die noch umzuziehen wäre. Offen bleiben die drei Kernstellen
|
||||
> (DPAPI, Pfadvergleiche, Zeitzonen-IDs) und ein `linux-x64`-Release (Roadmap DC3).
|
||||
|
||||
Frage: Was ist nötig, damit ClawdDotNet unter Linux läuft, und was kostet das?
|
||||
|
||||
---
|
||||
@@ -526,7 +534,7 @@ LiveCharts2 ist damit **keine Portierung, sondern neue Funktionalität** — sie
|
||||
gehört zum Trading-Teil, nicht zum Linux-Umzug, und ist in den 50–80 PT oben
|
||||
nicht enthalten. Wenn die Kursansichten kommen, sind dafür grob 5–10 PT
|
||||
zusätzlich zu rechnen. Das passt zu dem, was in
|
||||
[docs/Roadmap.md](docs/Roadmap.md) und der Notiz „Basis vor Trading härten"
|
||||
[docs/Roadmap.md](../Roadmap.md) und der Notiz „Basis vor Trading härten"
|
||||
festgehalten ist: Erst die Basis, dann die Handelsansichten.
|
||||
|
||||
Ein Punkt, der jetzt schon zählt: LiveCharts2 setzt auf SkiaSharp, genau wie
|
||||
@@ -14,7 +14,7 @@ denselben Code betreffen — die sollten in einem Zug mit erledigt werden.
|
||||
|
||||
### 1.1 Der Rechnername steckt im Hash — das ist das eigentliche Problem
|
||||
|
||||
[HardwareId.cs:43](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs):
|
||||
[HardwareId.cs:43](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs):
|
||||
|
||||
```csharp
|
||||
rawBuilder.Append(Environment.MachineName); // "for system isolation"
|
||||
@@ -31,12 +31,12 @@ Die Lizenz wäre nach dem zweiten `docker run` verbraucht.
|
||||
|
||||
Die Absicht („system isolation") ist auch nicht erfüllt: Der Rechnername steht
|
||||
ohnehin im Feld `hostname`, das der Server bei jeder Prüfung mitschreibt
|
||||
([LicenseService.php:112](../../LicenseLabrador/server/src/LicenseService.php)).
|
||||
([LicenseService.php:112](../../../LicenseLabrador/server/src/LicenseService.php)).
|
||||
Diagnostisch verlieren wir nichts, wenn er aus dem Hash verschwindet.
|
||||
|
||||
### 1.2 Die MAC-Ausweichlösung ist unter Linux instabil
|
||||
|
||||
[HardwareId.cs:86](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)
|
||||
[HardwareId.cs:86](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)
|
||||
nimmt die alphabetisch erste physische MAC. Unter Linux:
|
||||
|
||||
- Die Stoppwortliste kennt `docker` und `veth`, aber **nicht** `br-` (Bridges),
|
||||
@@ -50,7 +50,7 @@ Ausweichlösung ist damit unter Linux schlimmer als keine.
|
||||
|
||||
### 1.3 Der Zustandsspeicher fällt still auf Klartext zurück
|
||||
|
||||
[StateStore.cs:41](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/StateStore.cs)
|
||||
[StateStore.cs:41](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/StateStore.cs)
|
||||
beim Lesen und `:90` beim Schreiben:
|
||||
|
||||
```csharp
|
||||
@@ -63,17 +63,17 @@ Folgen:
|
||||
|
||||
- `SECURITY.md` behauptet, der Cache sei „strikt an die `hardware_id` gebunden".
|
||||
Das stimmt für die *Hülle* (die Prüfung in
|
||||
[LicenseClient.cs:198](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseClient.cs)),
|
||||
[LicenseClient.cs:198](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseClient.cs)),
|
||||
nicht für die Cache-Datei selbst.
|
||||
- Ernster: `max_seen_time` ist die Uhr-Rückdreh-Sperre
|
||||
([StateStore.cs:107](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/StateStore.cs)).
|
||||
([StateStore.cs:107](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/StateStore.cs)).
|
||||
Wer eine `state.dat` von Hand schreiben kann, setzt den Wert auf 0 und stellt
|
||||
die Systemuhr zurück. Der Klartext-Rückfall beim **Lesen** macht das möglich,
|
||||
und zwar auf jeder Plattform, auf der DPAPI nicht greift.
|
||||
|
||||
### 1.4 Ablageort bricht bei einem systemd-Dienst weg
|
||||
|
||||
[LicenseConfig.cs:22](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseConfig.cs)
|
||||
[LicenseConfig.cs:22](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseConfig.cs)
|
||||
verwendet `Environment.GetFolderPath(SpecialFolder.ApplicationData)`. Läuft der
|
||||
Dienst unter `User=clawd` ohne Heimatverzeichnis, ist `HOME` nicht gesetzt und
|
||||
`GetFolderPath` liefert einen **leeren String**. `Path.Combine("", slug,
|
||||
@@ -255,7 +255,7 @@ hwid_source = "machine-id",
|
||||
```
|
||||
|
||||
**Server** — in `LicenseService::validate`, an der Stelle der heutigen Suche
|
||||
([LicenseService.php:100](../../LicenseLabrador/server/src/LicenseService.php)):
|
||||
([LicenseService.php:100](../../../LicenseLabrador/server/src/LicenseService.php)):
|
||||
|
||||
```
|
||||
1. Aktivierung mit hardware_id = <v2> suchen
|
||||
@@ -281,7 +281,7 @@ Veröffentlichungen) aus dem Client fallen.
|
||||
Nicht übersehen: Die Hardware-ID geht in zwei weitere Berechnungen ein —
|
||||
`CalculateHmac(state, licenseKey, _hardwareId)` für die Prüfsumme in
|
||||
`LicenseResult`
|
||||
([LicenseClient.cs:272](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseClient.cs))
|
||||
([LicenseClient.cs:272](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseClient.cs))
|
||||
und den Seed des Speicherschutzes (`:287`). Nach dem Formatwechsel schlägt
|
||||
`VerifyChecksum` für jede zwischengespeicherte Hülle fehl.
|
||||
|
||||
@@ -321,7 +321,7 @@ für den Fall danach.
|
||||
### 5.2 Abmelden vor dem Umzug (existiert, aber nicht erreichbar)
|
||||
|
||||
`LicenseService::deactivate` löscht die Aktivierungszeile und gibt den Platz frei
|
||||
([LicenseService.php:153](../../LicenseLabrador/server/src/LicenseService.php)),
|
||||
([LicenseService.php:153](../../../LicenseLabrador/server/src/LicenseService.php)),
|
||||
und `LicenseClient.DeactivateAsync` ruft es auf. In ClawdDotNet ist die Methode
|
||||
aber nur über [LicenseGate.cs:39](Services/LicenseGate.cs) erreichbar und dort
|
||||
an die GUI gebunden.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Archiv
|
||||
|
||||
Was hier liegt, ist **abgeschlossen und wird nicht mehr fortgeschrieben.** Der aktuelle
|
||||
Stand steht ausschließlich in der [Roadmap](../Roadmap.md); ihr Abschnitt 7 hält fest,
|
||||
was aus jedem dieser Dokumente dorthin übernommen wurde.
|
||||
|
||||
Aufgehoben werden sie, weil sie die **Begründung** tragen: warum das System so
|
||||
geschnitten ist, wie es geschnitten ist, und welche Befunde die Entscheidungen geformt
|
||||
haben. Diese Herleitung lässt sich in einer Vorhabenliste nicht unterbringen, ohne sie
|
||||
unlesbar zu machen.
|
||||
|
||||
> **Verweise auf Quelltext können ins Leere gehen.** Viele dieser Papiere zeigen auf
|
||||
> Dateien der WinForms-Fassung (`frm_*.cs`, `UI/`, `Models/`, `Program.cs`) oder auf die
|
||||
> alten Scheduler — beides wurde inzwischen entfernt. Wer die Stellen sehen will, findet
|
||||
> sie im Tag `vor-fruehjahrsputz-2026-08`. Die Zeilennummern wurden bewusst **nicht**
|
||||
> nachgeführt: Ein archiviertes Dokument beschreibt den Stand seines Datums.
|
||||
|
||||
---
|
||||
|
||||
## Befunde und Konzepte
|
||||
|
||||
| Dokument | Datum | Was es war | Warum archiviert |
|
||||
|---|---|---|---|
|
||||
| [Bestandsaufnahme-2026-07](Bestandsaufnahme-2026-07.md) | Juli 2026 | Vollständiges Review von Engine, Sicherheit, Tools, Scheduling und Oberfläche. Quelle der Kürzel S1–S7, B1–B14, K1–K6, T1–T9, F-A1…F-A7 | Erledigtes steht in Roadmap 5, Offenes in 3.1/3.2/3.7. Die Kürzel leben in der Herkunft-Spalte weiter |
|
||||
| [Konzepte-Backup-Finanz-Analyse](Konzepte-Backup-Finanz-Analyse.md) | Juli 2026 | Drei Konzepte: Sicherung, Finanzumfeld, Leistungsmessung | Sicherung ist gebaut; der Rest läuft als C1–C8 weiter |
|
||||
| [Linux-Portierung-Analyse](Linux-Portierung-Analyse.md) | 2026-08-06 | Was kostet der Umzug nach Linux | Der teure Teil — 8.900 Zeilen WinForms — ist mit der Avalonia-Portierung entfallen. Übrig bleiben drei Kernstellen (DPAPI, Pfadvergleiche, Zeitzonen-IDs), sie stehen in Roadmap 3.5 |
|
||||
| [Lizenz-HardwareId-v2](Lizenz-HardwareId-v2-Implementierungsvorschlag.md) | 2026-08-06 | Überarbeitung der Hardware-Erkennung für LicenseLabrador | **Gegenstandslos.** LicenseLabrador ist durch das Deploymentcenter ersetzt. Lesenswert bleibt Abschnitt 1: warum der Rechnername nicht in eine Hardware-Kennung gehört |
|
||||
| [Deploymentcenter-Anbindung-Review](Deploymentcenter-Anbindung-Review.md) | 2026-08-08 | Review der ersten Anbindung | Befunde behoben; die Verdrahtung beschreibt heute [Deploymentcenter-Integration](../Deploymentcenter-Integration.md) |
|
||||
| [Deploymentcenter-2.4-Integrationsplan](Deploymentcenter-2.4-Integrationsplan.md) | 2026-08-15 | Zugangsschutz, Release-Strecke, Update, Erstinstallation, Signatur — durchgearbeitet | Abgearbeitet bis auf DC8 und zwei Betreiberpunkte; die stehen in Roadmap 3.5. Enthält die Messprotokolle der live durchgespielten Setup- und Update-Kette |
|
||||
|
||||
## Entwicklungs-Prompts der Anfangszeit
|
||||
|
||||
Mai–Juli 2026. Sie haben ClawdDotNet aufgebaut und beschreiben deshalb den Stand von
|
||||
damals — unter anderem eine WinForms-Oberfläche mit WebView2, die es nicht mehr gibt.
|
||||
|
||||
| Dokument | Was darin steht | Was davon noch gilt |
|
||||
|---|---|---|
|
||||
| [ClawdDotNet_StartPrompt](ClawdDotNet_StartPrompt.md) | Gesamtentwurf, Kernklassen, Beispielkonfiguration | Der Schichtschnitt Core/Tools/App und der Tool-Vertrag stammen von hier. Oberfläche und `configs/*.json` sind überholt |
|
||||
| [ClawdDotNet_Prompt_WebviewChatWinForms](ClawdDotNet_Prompt_WebviewChatWinForms.md) | WinForms-Oberfläche mit WebView2-Chat | nichts — ersetzt durch `src/ClawdDotNet.Desktop` |
|
||||
| [ClawdDotNet_Prompt_TelegramClient](ClawdDotNet_Prompt_TelegramClient.md) | Entwurf des TelegramClient-Tools | umgesetzt in `src/ClawdDotNet.Tools.TelegramClient` |
|
||||
| [ClawdDotNet_Prompt_InternetTools](ClawdDotNet_Prompt_InternetTools.md) | WebFetch, DirectAPI, WebMonitor | **Eine Regel gilt weiter:** die Pflichtfelder `fetchedAt` / `dataAsOf` / `source` in jedem Tool-Ergebnis mit externen Daten. Der [WebSearch-Umsetzungsplan](../umsetzungsplaene/UMSETZUNGSPLAN-WebSearch-Tool.md) verweist darauf. Zieht diese Regel in ein eigenes Dokument um, kann die Datei ganz weg |
|
||||
@@ -1,5 +1,9 @@
|
||||
# Umsetzungsplan: AgentEditor härten (Personalverwaltung)
|
||||
|
||||
> **Stand 2026-08-23: beschlossen, noch nicht gebaut.** Einordnung siehe
|
||||
> [Roadmap](../Roadmap.md) 3.2 (nach AgentInspector) — die Reihenfolge gilt dort,
|
||||
> dieses Dokument ist der Bauplan.
|
||||
|
||||
> Stand: 2026-08-05
|
||||
> Ziel: Änderungen an Identity und Soul im laufenden Betrieb bleiben möglich,
|
||||
> werden aber freigabepflichtig, nachvollziehbar und rücknehmbar.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Umsetzungsplan: AgentInspector (Supervisor-Einsicht)
|
||||
|
||||
> **Stand 2026-08-23: beschlossen, noch nicht gebaut.** Einordnung siehe
|
||||
> [Roadmap](../Roadmap.md) 3.2 — die Reihenfolge gilt dort, dieses Dokument ist der Bauplan.
|
||||
|
||||
> Stand: 2026-08-05
|
||||
> Ziel: Ein Supervisor-Agent kann beurteilen, ob die anderen Agenten der Instanz
|
||||
> das tun, was sie tun sollen — über Audit-Log, Taskboard und lesenden Zugriff
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Umsetzungsplan: FileRW-Papierkorb + Cleanup-Job
|
||||
|
||||
> **Stand 2026-08-23: beschlossen, noch nicht gebaut.** Eingeordnet als Punkt 3 der
|
||||
> [Roadmap](../Roadmap.md) — die Reihenfolge gilt dort, dieses Dokument ist der Bauplan.
|
||||
|
||||
> Stand: 2026-08-05
|
||||
> Ziel: `FileRW.delete` löscht nicht mehr endgültig, sondern verschiebt in einen
|
||||
> Papierkorb je Workspace. Ein Cron-Job räumt den Papierkorb nach X Tagen auf.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Umsetzungsplan: WebSearch-Tool (Internetzugang erweitern)
|
||||
|
||||
> **Stand 2026-08-23: nicht beschlossen.** Steht im Ideenspeicher der
|
||||
> [Roadmap](../Roadmap.md) (Abschnitt 4) und bleibt bewusst zuletzt — größter
|
||||
> Sicherheitshebel. Dieses Dokument ist der Bauplan, falls entschieden wird.
|
||||
|
||||
> Stand: 2026-08-05
|
||||
> Ziel: Agenten können das Web durchsuchen, statt nur bekannte URLs abzurufen.
|
||||
> Reihenfolge: **Zuletzt.** Größter Sicherheitshebel, deshalb erst nach
|
||||
@@ -20,7 +24,7 @@ Internetzugang ist bereits vorhanden, aber nur in eine Richtung:
|
||||
| `WebMonitor` | strukturierte Seiten überwachen | feste Zielseiten |
|
||||
|
||||
Alle drei liefern die Pflichtfelder `fetchedAt` / `dataAsOf` / `source` aus
|
||||
`ClawdDotNet_Prompt_InternetTools.md` und sind gegen SSRF abgesichert
|
||||
`docs/archiv/ClawdDotNet_Prompt_InternetTools.md` und sind gegen SSRF abgesichert
|
||||
(`UrlGuard` prüft Schema, private Netze und Whitelist, auch über Redirects
|
||||
hinweg — `UrlSanitizer` analog für DirectAPI).
|
||||
|
||||
@@ -153,7 +157,7 @@ Ergänzend im Tool:
|
||||
|
||||
### Slice 3 — Betrieb
|
||||
- [ ] Rechercheagent-Vorlage in `docs/InstanceSetupGuide.md` gemäß Abschnitt 4
|
||||
- [ ] `ClawdDotNet_Prompt_InternetTools.md` um das Tool ergänzen
|
||||
- [ ] `docs/archiv/ClawdDotNet_Prompt_InternetTools.md` um das Tool ergänzen
|
||||
- [ ] `docs/ToolDevelopmentGuide.md`: Steckbrief
|
||||
|
||||
---
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
namespace ClawdDotNet
|
||||
{
|
||||
partial class frm_AddJob
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
lblJobType = new Label();
|
||||
cbJobType = new ComboBox();
|
||||
lblAgent = new Label();
|
||||
cbAgent = new ComboBox();
|
||||
lblTool = new Label();
|
||||
cbTool = new ComboBox();
|
||||
lblJobDef = new Label();
|
||||
cbJobDef = new ComboBox();
|
||||
lblCron = new Label();
|
||||
txtCron = new TextBox();
|
||||
lblPreview = new Label();
|
||||
lblTask = new Label();
|
||||
txtTask = new TextBox();
|
||||
chkRunOnStart = new CheckBox();
|
||||
lblHelp = new Label();
|
||||
btnOk = new Button();
|
||||
btnCancel = new Button();
|
||||
SuspendLayout();
|
||||
//
|
||||
// lblJobType
|
||||
//
|
||||
lblJobType.AutoSize = true;
|
||||
lblJobType.Location = new Point(12, 9);
|
||||
lblJobType.Name = "lblJobType";
|
||||
lblJobType.Size = new Size(64, 25);
|
||||
lblJobType.TabIndex = 0;
|
||||
lblJobType.Text = "Job-Typ:";
|
||||
//
|
||||
// cbJobType
|
||||
//
|
||||
cbJobType.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
cbJobType.Items.AddRange(new object[] { "Agent Wakeup", "Tool Job" });
|
||||
cbJobType.Location = new Point(130, 6);
|
||||
cbJobType.Name = "cbJobType";
|
||||
cbJobType.Size = new Size(326, 33);
|
||||
cbJobType.TabIndex = 1;
|
||||
cbJobType.SelectedIndexChanged += OnJobTypeChanged;
|
||||
//
|
||||
// lblAgent
|
||||
//
|
||||
lblAgent.AutoSize = true;
|
||||
lblAgent.Location = new Point(12, 48);
|
||||
lblAgent.Name = "lblAgent";
|
||||
lblAgent.Size = new Size(64, 25);
|
||||
lblAgent.TabIndex = 2;
|
||||
lblAgent.Text = "Agent:";
|
||||
//
|
||||
// cbAgent
|
||||
//
|
||||
cbAgent.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
cbAgent.Location = new Point(130, 45);
|
||||
cbAgent.Name = "cbAgent";
|
||||
cbAgent.Size = new Size(326, 33);
|
||||
cbAgent.TabIndex = 3;
|
||||
//
|
||||
// lblTool
|
||||
//
|
||||
lblTool.AutoSize = true;
|
||||
lblTool.Location = new Point(12, 87);
|
||||
lblTool.Name = "lblTool";
|
||||
lblTool.Size = new Size(45, 25);
|
||||
lblTool.TabIndex = 4;
|
||||
lblTool.Text = "Tool:";
|
||||
//
|
||||
// cbTool
|
||||
//
|
||||
cbTool.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
cbTool.Location = new Point(130, 84);
|
||||
cbTool.Name = "cbTool";
|
||||
cbTool.Size = new Size(326, 33);
|
||||
cbTool.TabIndex = 5;
|
||||
cbTool.SelectedIndexChanged += OnToolChanged;
|
||||
//
|
||||
// lblJobDef
|
||||
//
|
||||
lblJobDef.AutoSize = true;
|
||||
lblJobDef.Location = new Point(12, 126);
|
||||
lblJobDef.Name = "lblJobDef";
|
||||
lblJobDef.Size = new Size(64, 25);
|
||||
lblJobDef.TabIndex = 6;
|
||||
lblJobDef.Text = "Job:";
|
||||
//
|
||||
// cbJobDef
|
||||
//
|
||||
cbJobDef.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
cbJobDef.Location = new Point(130, 123);
|
||||
cbJobDef.Name = "cbJobDef";
|
||||
cbJobDef.Size = new Size(326, 33);
|
||||
cbJobDef.TabIndex = 7;
|
||||
//
|
||||
// lblCron
|
||||
//
|
||||
lblCron.AutoSize = true;
|
||||
lblCron.Location = new Point(12, 168);
|
||||
lblCron.Name = "lblCron";
|
||||
lblCron.Size = new Size(79, 25);
|
||||
lblCron.TabIndex = 8;
|
||||
lblCron.Text = "Zeitplan:";
|
||||
//
|
||||
// txtCron
|
||||
//
|
||||
txtCron.Location = new Point(130, 165);
|
||||
txtCron.Name = "txtCron";
|
||||
txtCron.PlaceholderText = "z.B. */2 * * * * (alle 2 Min)";
|
||||
txtCron.Size = new Size(326, 31);
|
||||
txtCron.TabIndex = 9;
|
||||
txtCron.TextChanged += OnCronChanged;
|
||||
//
|
||||
// lblPreview
|
||||
//
|
||||
lblPreview.Font = new Font("Segoe UI", 8F);
|
||||
lblPreview.ForeColor = Color.Gray;
|
||||
lblPreview.Location = new Point(130, 199);
|
||||
lblPreview.Name = "lblPreview";
|
||||
lblPreview.Size = new Size(326, 20);
|
||||
lblPreview.TabIndex = 10;
|
||||
//
|
||||
// lblTask
|
||||
//
|
||||
lblTask.AutoSize = true;
|
||||
lblTask.Location = new Point(12, 226);
|
||||
lblTask.Name = "lblTask";
|
||||
lblTask.Size = new Size(84, 25);
|
||||
lblTask.TabIndex = 11;
|
||||
lblTask.Text = "Aufgabe:";
|
||||
//
|
||||
// txtTask
|
||||
//
|
||||
txtTask.Location = new Point(130, 223);
|
||||
txtTask.Multiline = true;
|
||||
txtTask.Name = "txtTask";
|
||||
txtTask.ScrollBars = ScrollBars.Vertical;
|
||||
txtTask.Size = new Size(326, 80);
|
||||
txtTask.TabIndex = 12;
|
||||
txtTask.Text = "Führe deine zugewiesenen Aufgaben aus.";
|
||||
//
|
||||
// chkRunOnStart
|
||||
//
|
||||
chkRunOnStart.AutoSize = true;
|
||||
chkRunOnStart.Location = new Point(12, 312);
|
||||
chkRunOnStart.Name = "chkRunOnStart";
|
||||
chkRunOnStart.Size = new Size(395, 29);
|
||||
chkRunOnStart.TabIndex = 13;
|
||||
chkRunOnStart.Text = "Beim Programmstart sofort einmal ausführen";
|
||||
//
|
||||
// lblHelp
|
||||
//
|
||||
lblHelp.Font = new Font("Segoe UI", 8F);
|
||||
lblHelp.ForeColor = Color.DimGray;
|
||||
lblHelp.Location = new Point(12, 348);
|
||||
lblHelp.Name = "lblHelp";
|
||||
lblHelp.Size = new Size(444, 60);
|
||||
lblHelp.TabIndex = 14;
|
||||
lblHelp.Text = "Cron-Format: Min Std Tag Mon Wochentag\r\nBeispiele: */5 * * * * = alle 5 Min\r\n 0 8 * * 1-5 = Mo-Fr um 08:00\r\n 0 */2 * * * = alle 2 Stunden";
|
||||
//
|
||||
// btnOk
|
||||
//
|
||||
btnOk.DialogResult = DialogResult.OK;
|
||||
btnOk.Location = new Point(240, 418);
|
||||
btnOk.Name = "btnOk";
|
||||
btnOk.Size = new Size(106, 38);
|
||||
btnOk.TabIndex = 15;
|
||||
btnOk.Text = "Hinzufügen";
|
||||
btnOk.Click += OnOkClick;
|
||||
//
|
||||
// btnCancel
|
||||
//
|
||||
btnCancel.DialogResult = DialogResult.Cancel;
|
||||
btnCancel.Location = new Point(356, 418);
|
||||
btnCancel.Name = "btnCancel";
|
||||
btnCancel.Size = new Size(100, 38);
|
||||
btnCancel.TabIndex = 16;
|
||||
btnCancel.Text = "Abbrechen";
|
||||
//
|
||||
// frm_AddJob
|
||||
//
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
ClientSize = new Size(470, 470);
|
||||
Controls.Add(lblJobType);
|
||||
Controls.Add(cbJobType);
|
||||
Controls.Add(lblAgent);
|
||||
Controls.Add(cbAgent);
|
||||
Controls.Add(lblTool);
|
||||
Controls.Add(cbTool);
|
||||
Controls.Add(lblJobDef);
|
||||
Controls.Add(cbJobDef);
|
||||
Controls.Add(lblCron);
|
||||
Controls.Add(txtCron);
|
||||
Controls.Add(lblPreview);
|
||||
Controls.Add(lblTask);
|
||||
Controls.Add(txtTask);
|
||||
Controls.Add(chkRunOnStart);
|
||||
Controls.Add(lblHelp);
|
||||
Controls.Add(btnOk);
|
||||
Controls.Add(btnCancel);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "frm_AddJob";
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Job hinzufügen";
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Label lblJobType;
|
||||
private ComboBox cbJobType;
|
||||
private Label lblAgent;
|
||||
private ComboBox cbAgent;
|
||||
private Label lblTool;
|
||||
private ComboBox cbTool;
|
||||
private Label lblJobDef;
|
||||
private ComboBox cbJobDef;
|
||||
private Label lblCron;
|
||||
private TextBox txtCron;
|
||||
private Label lblPreview;
|
||||
private Label lblTask;
|
||||
private TextBox txtTask;
|
||||
private CheckBox chkRunOnStart;
|
||||
private Label lblHelp;
|
||||
private Button btnOk;
|
||||
private Button btnCancel;
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Scheduling;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
|
||||
namespace ClawdDotNet;
|
||||
|
||||
public sealed partial class frm_AddJob : Form
|
||||
{
|
||||
private readonly IReadOnlyList<AgentConfig> _agents;
|
||||
private readonly IReadOnlyList<(IAgentTool Tool, IToolJobProvider Provider)> _jobProviders;
|
||||
|
||||
// ─── Output Properties ───
|
||||
|
||||
public bool IsToolJob => cbJobType.SelectedIndex == 1;
|
||||
public string SelectedAgentId => (cbAgent.SelectedItem as AgentComboItem)?.AgentId ?? "";
|
||||
public string CronExpression => txtCron.Text.Trim();
|
||||
public string TaskMessage => txtTask.Text.Trim();
|
||||
public bool RunOnStart => chkRunOnStart.Checked;
|
||||
public string SelectedToolName => (cbTool.SelectedItem as ToolComboItem)?.ToolName ?? "";
|
||||
public string SelectedJobTypeId => (cbJobDef.SelectedItem as JobDefComboItem)?.JobTypeId ?? "";
|
||||
|
||||
/// <summary>Konstruktor für "Hinzufügen"</summary>
|
||||
public frm_AddJob(IReadOnlyList<AgentConfig> agents, ToolRegistry toolRegistry)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_agents = agents;
|
||||
_jobProviders = toolRegistry.GetJobProviders();
|
||||
|
||||
foreach (var agent in agents)
|
||||
cbAgent.Items.Add(new AgentComboItem(agent.AgentId, agent.DisplayName));
|
||||
if (cbAgent.Items.Count > 0) cbAgent.SelectedIndex = 0;
|
||||
|
||||
foreach (var (tool, _) in _jobProviders)
|
||||
cbTool.Items.Add(new ToolComboItem(tool.Name, tool.Description));
|
||||
if (cbTool.Items.Count > 0) cbTool.SelectedIndex = 0;
|
||||
|
||||
cbJobType.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Konstruktor für "Bearbeiten" — Agent, Tool und Job-Typ sind gesperrt</summary>
|
||||
public frm_AddJob(IReadOnlyList<AgentConfig> agents, ToolRegistry toolRegistry,
|
||||
string agentId, string cron, string taskMessage, bool runOnStart,
|
||||
bool isToolJob, string? toolName = null, string? jobTypeId = null)
|
||||
: this(agents, toolRegistry)
|
||||
{
|
||||
Text = "Job bearbeiten";
|
||||
btnOk.Text = "Speichern";
|
||||
|
||||
// Job-Typ setzen und sperren
|
||||
cbJobType.SelectedIndex = isToolJob ? 1 : 0;
|
||||
cbJobType.Enabled = false;
|
||||
|
||||
// Agent vorauswählen und sperren
|
||||
for (int i = 0; i < cbAgent.Items.Count; i++)
|
||||
{
|
||||
if (((AgentComboItem)cbAgent.Items[i]!).AgentId == agentId)
|
||||
{
|
||||
cbAgent.SelectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cbAgent.Enabled = false;
|
||||
|
||||
// Bearbeitbare Felder befüllen
|
||||
txtCron.Text = cron;
|
||||
chkRunOnStart.Checked = runOnStart;
|
||||
|
||||
if (isToolJob && toolName is not null)
|
||||
{
|
||||
// Tool vorauswählen und sperren
|
||||
for (int i = 0; i < cbTool.Items.Count; i++)
|
||||
{
|
||||
if (((ToolComboItem)cbTool.Items[i]!).ToolName == toolName)
|
||||
{
|
||||
cbTool.SelectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cbTool.Enabled = false;
|
||||
|
||||
// Job-Definition vorauswählen und sperren
|
||||
if (jobTypeId is not null)
|
||||
{
|
||||
for (int i = 0; i < cbJobDef.Items.Count; i++)
|
||||
{
|
||||
if (((JobDefComboItem)cbJobDef.Items[i]!).JobTypeId == jobTypeId)
|
||||
{
|
||||
cbJobDef.SelectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
cbJobDef.Enabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
txtTask.Text = taskMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnJobTypeChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var isToolJob = cbJobType.SelectedIndex == 1;
|
||||
|
||||
lblTool.Visible = isToolJob;
|
||||
cbTool.Visible = isToolJob;
|
||||
lblJobDef.Visible = isToolJob;
|
||||
cbJobDef.Visible = isToolJob;
|
||||
|
||||
lblTask.Visible = !isToolJob;
|
||||
txtTask.Visible = !isToolJob;
|
||||
|
||||
RefreshAgentList();
|
||||
}
|
||||
|
||||
private void OnToolChanged(object? sender, EventArgs e)
|
||||
{
|
||||
cbJobDef.Items.Clear();
|
||||
|
||||
if (cbTool.SelectedItem is not ToolComboItem toolItem)
|
||||
return;
|
||||
|
||||
var provider = _jobProviders.FirstOrDefault(p => ((IAgentTool)p.Provider).Name == toolItem.ToolName);
|
||||
if (provider.Provider is null)
|
||||
return;
|
||||
|
||||
foreach (var def in provider.Provider.GetJobDefinitions())
|
||||
cbJobDef.Items.Add(new JobDefComboItem(def.JobTypeId, def.DisplayName, def.Description));
|
||||
|
||||
if (cbJobDef.Items.Count > 0) cbJobDef.SelectedIndex = 0;
|
||||
|
||||
RefreshAgentList();
|
||||
}
|
||||
|
||||
private void RefreshAgentList()
|
||||
{
|
||||
var previousAgentId = (cbAgent.SelectedItem as AgentComboItem)?.AgentId;
|
||||
cbAgent.Items.Clear();
|
||||
|
||||
var toolName = IsToolJob && cbTool.SelectedItem is ToolComboItem toolItem
|
||||
? toolItem.ToolName : null;
|
||||
|
||||
foreach (var agent in _agents)
|
||||
{
|
||||
if (toolName is not null && !agent.Tools.ContainsKey(toolName))
|
||||
continue;
|
||||
|
||||
cbAgent.Items.Add(new AgentComboItem(agent.AgentId, agent.DisplayName));
|
||||
}
|
||||
|
||||
if (previousAgentId is not null)
|
||||
{
|
||||
for (int i = 0; i < cbAgent.Items.Count; i++)
|
||||
{
|
||||
if (((AgentComboItem)cbAgent.Items[i]!).AgentId == previousAgentId)
|
||||
{
|
||||
cbAgent.SelectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cbAgent.SelectedIndex < 0 && cbAgent.Items.Count > 0)
|
||||
cbAgent.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void OnCronChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var text = txtCron.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
lblPreview.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cron = Core.Scheduling.CronExpression.Parse(text);
|
||||
var next = cron.GetNextOccurrence(DateTime.Now);
|
||||
lblPreview.Text = next is not null
|
||||
? $"Nächste Ausführung: {next:dd.MM.yyyy HH:mm}"
|
||||
: "Kein nächster Zeitpunkt";
|
||||
lblPreview.ForeColor = Color.Green;
|
||||
}
|
||||
catch
|
||||
{
|
||||
lblPreview.Text = "Ungültiger Cron-Ausdruck";
|
||||
lblPreview.ForeColor = Color.Red;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOkClick(object? sender, EventArgs e)
|
||||
{
|
||||
if (cbAgent.SelectedItem is null)
|
||||
{
|
||||
MessageBox.Show("Bitte einen Agenten auswählen.", "Validierung",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(txtCron.Text))
|
||||
{
|
||||
MessageBox.Show("Bitte einen Cron-Ausdruck eingeben.", "Validierung",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Core.Scheduling.CronExpression.Parse(txtCron.Text.Trim());
|
||||
}
|
||||
catch
|
||||
{
|
||||
MessageBox.Show("Ungültiger Cron-Ausdruck.", "Validierung",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsToolJob)
|
||||
{
|
||||
if (cbTool.SelectedItem is null)
|
||||
{
|
||||
MessageBox.Show("Bitte ein Tool auswählen.", "Validierung",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cbJobDef.SelectedItem is null)
|
||||
{
|
||||
MessageBox.Show("Bitte einen Job-Typ auswählen.", "Validierung",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ComboBox Items ───
|
||||
|
||||
private sealed record AgentComboItem(string AgentId, string DisplayName)
|
||||
{
|
||||
public override string ToString() => DisplayName;
|
||||
}
|
||||
|
||||
private sealed record ToolComboItem(string ToolName, string Description)
|
||||
{
|
||||
public override string ToString() => ToolName;
|
||||
}
|
||||
|
||||
private sealed record JobDefComboItem(string JobTypeId, string DisplayName, string Description)
|
||||
{
|
||||
public override string ToString() => $"{DisplayName} ({JobTypeId})";
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,157 +0,0 @@
|
||||
namespace ClawdDotNet
|
||||
{
|
||||
partial class frm_AddService
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
lblType = new Label();
|
||||
cbType = new ComboBox();
|
||||
lblName = new Label();
|
||||
txtName = new TextBox();
|
||||
lblPort = new Label();
|
||||
txtPort = new TextBox();
|
||||
lblDesc = new Label();
|
||||
txtDescription = new TextBox();
|
||||
btnOk = new Button();
|
||||
btnCancel = new Button();
|
||||
SuspendLayout();
|
||||
//
|
||||
// lblType
|
||||
//
|
||||
lblType.AutoSize = true;
|
||||
lblType.Location = new Point(16, 18);
|
||||
lblType.Name = "lblType";
|
||||
lblType.Size = new Size(85, 20);
|
||||
lblType.TabIndex = 0;
|
||||
lblType.Text = "Service-Typ:";
|
||||
//
|
||||
// cbType
|
||||
//
|
||||
cbType.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
cbType.Items.AddRange(new object[] { "StaticFileServer", "ReverseProxy", "Custom" });
|
||||
cbType.Location = new Point(140, 15);
|
||||
cbType.Name = "cbType";
|
||||
cbType.Size = new Size(260, 28);
|
||||
cbType.TabIndex = 1;
|
||||
cbType.SelectedIndexChanged += OnTypeChanged;
|
||||
//
|
||||
// lblName
|
||||
//
|
||||
lblName.AutoSize = true;
|
||||
lblName.Location = new Point(16, 58);
|
||||
lblName.Name = "lblName";
|
||||
lblName.Size = new Size(49, 20);
|
||||
lblName.TabIndex = 2;
|
||||
lblName.Text = "Name:";
|
||||
//
|
||||
// txtName
|
||||
//
|
||||
txtName.Location = new Point(140, 55);
|
||||
txtName.Name = "txtName";
|
||||
txtName.Size = new Size(260, 27);
|
||||
txtName.TabIndex = 3;
|
||||
//
|
||||
// lblPort
|
||||
//
|
||||
lblPort.AutoSize = true;
|
||||
lblPort.Location = new Point(16, 98);
|
||||
lblPort.Name = "lblPort";
|
||||
lblPort.Size = new Size(37, 20);
|
||||
lblPort.TabIndex = 4;
|
||||
lblPort.Text = "Port:";
|
||||
//
|
||||
// txtPort
|
||||
//
|
||||
txtPort.Location = new Point(140, 95);
|
||||
txtPort.Name = "txtPort";
|
||||
txtPort.Size = new Size(100, 27);
|
||||
txtPort.TabIndex = 5;
|
||||
txtPort.Text = "8090";
|
||||
//
|
||||
// lblDesc
|
||||
//
|
||||
lblDesc.AutoSize = true;
|
||||
lblDesc.Location = new Point(16, 138);
|
||||
lblDesc.Name = "lblDesc";
|
||||
lblDesc.Size = new Size(101, 20);
|
||||
lblDesc.TabIndex = 6;
|
||||
lblDesc.Text = "Beschreibung:";
|
||||
//
|
||||
// txtDescription
|
||||
//
|
||||
txtDescription.Location = new Point(140, 135);
|
||||
txtDescription.Name = "txtDescription";
|
||||
txtDescription.Size = new Size(260, 27);
|
||||
txtDescription.TabIndex = 7;
|
||||
//
|
||||
// btnOk
|
||||
//
|
||||
btnOk.Location = new Point(200, 185);
|
||||
btnOk.Name = "btnOk";
|
||||
btnOk.Size = new Size(100, 30);
|
||||
btnOk.TabIndex = 8;
|
||||
btnOk.Text = "Hinzufügen";
|
||||
btnOk.DialogResult = DialogResult.OK;
|
||||
btnOk.Click += OnOkClick;
|
||||
//
|
||||
// btnCancel
|
||||
//
|
||||
btnCancel.Location = new Point(310, 185);
|
||||
btnCancel.Name = "btnCancel";
|
||||
btnCancel.Size = new Size(90, 30);
|
||||
btnCancel.TabIndex = 9;
|
||||
btnCancel.Text = "Abbrechen";
|
||||
btnCancel.DialogResult = DialogResult.Cancel;
|
||||
//
|
||||
// frm_AddService
|
||||
//
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
ClientSize = new Size(420, 230);
|
||||
Controls.Add(lblType);
|
||||
Controls.Add(cbType);
|
||||
Controls.Add(lblName);
|
||||
Controls.Add(txtName);
|
||||
Controls.Add(lblPort);
|
||||
Controls.Add(txtPort);
|
||||
Controls.Add(lblDesc);
|
||||
Controls.Add(txtDescription);
|
||||
Controls.Add(btnOk);
|
||||
Controls.Add(btnCancel);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "frm_AddService";
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Service hinzufügen";
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Label lblType;
|
||||
private ComboBox cbType;
|
||||
private Label lblName;
|
||||
private TextBox txtName;
|
||||
private Label lblPort;
|
||||
private TextBox txtPort;
|
||||
private Label lblDesc;
|
||||
private TextBox txtDescription;
|
||||
private Button btnOk;
|
||||
private Button btnCancel;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
namespace ClawdDotNet;
|
||||
|
||||
public sealed partial class frm_AddService : Form
|
||||
{
|
||||
public string ServiceName => txtName.Text.Trim();
|
||||
public string ServiceType => cbType.SelectedItem?.ToString() ?? "Custom";
|
||||
public int ServicePort => int.TryParse(txtPort.Text, out var p) ? p : 0;
|
||||
public string ServiceDescription => txtDescription.Text.Trim();
|
||||
|
||||
public frm_AddService()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
cbType.SelectedIndex = 0;
|
||||
OnTypeChanged(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void OnTypeChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var type = cbType.SelectedItem?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(txtName.Text) || txtName.Text.StartsWith("Neuer "))
|
||||
txtName.Text = $"Neuer {type}";
|
||||
|
||||
txtDescription.Text = type switch
|
||||
{
|
||||
"StaticFileServer" => "Statischer Datei-Server",
|
||||
"ReverseProxy" => "Reverse-Proxy zu einem externen Service",
|
||||
"Custom" => "Benutzerdefinierter Service",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private void OnOkClick(object? sender, EventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(txtName.Text))
|
||||
{
|
||||
MessageBox.Show("Bitte einen Namen eingeben.", "Validierung",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(txtPort.Text, out var port) || port < 1 || port > 65535)
|
||||
{
|
||||
MessageBox.Show("Bitte einen gültigen Port (1-65535) eingeben.", "Validierung",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,111 +0,0 @@
|
||||
namespace ClawdDotNet
|
||||
{
|
||||
partial class frm_CreateInstance
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
lblInfo = new Label();
|
||||
txtName = new TextBox();
|
||||
lblPreview = new Label();
|
||||
btnOk = new Button();
|
||||
btnCancel = new Button();
|
||||
SuspendLayout();
|
||||
//
|
||||
// lblInfo
|
||||
//
|
||||
lblInfo.Font = new Font("Segoe UI", 10F);
|
||||
lblInfo.ForeColor = Color.White;
|
||||
lblInfo.Location = new Point(20, 20);
|
||||
lblInfo.Name = "lblInfo";
|
||||
lblInfo.Size = new Size(380, 25);
|
||||
lblInfo.TabIndex = 0;
|
||||
lblInfo.Text = "Name der neuen Instanz:";
|
||||
//
|
||||
// txtName
|
||||
//
|
||||
txtName.Font = new Font("Segoe UI", 11F);
|
||||
txtName.Location = new Point(20, 50);
|
||||
txtName.Name = "txtName";
|
||||
txtName.Size = new Size(390, 32);
|
||||
txtName.TabIndex = 1;
|
||||
txtName.TextChanged += OnTxtNameTextChanged;
|
||||
//
|
||||
// lblPreview
|
||||
//
|
||||
lblPreview.Font = new Font("Segoe UI", 9F);
|
||||
lblPreview.ForeColor = Color.Gray;
|
||||
lblPreview.Location = new Point(20, 90);
|
||||
lblPreview.Name = "lblPreview";
|
||||
lblPreview.Size = new Size(390, 25);
|
||||
lblPreview.TabIndex = 2;
|
||||
lblPreview.Text = "Ordner: Instance-...";
|
||||
//
|
||||
// btnOk
|
||||
//
|
||||
btnOk.BackColor = Color.FromArgb(60, 130, 60);
|
||||
btnOk.FlatStyle = FlatStyle.Flat;
|
||||
btnOk.ForeColor = Color.White;
|
||||
btnOk.Location = new Point(220, 130);
|
||||
btnOk.Name = "btnOk";
|
||||
btnOk.Size = new Size(90, 35);
|
||||
btnOk.TabIndex = 3;
|
||||
btnOk.Text = "Erstellen";
|
||||
btnOk.UseVisualStyleBackColor = false;
|
||||
btnOk.DialogResult = DialogResult.OK;
|
||||
//
|
||||
// btnCancel
|
||||
//
|
||||
btnCancel.BackColor = Color.FromArgb(80, 80, 80);
|
||||
btnCancel.FlatStyle = FlatStyle.Flat;
|
||||
btnCancel.ForeColor = Color.White;
|
||||
btnCancel.Location = new Point(320, 130);
|
||||
btnCancel.Name = "btnCancel";
|
||||
btnCancel.Size = new Size(90, 35);
|
||||
btnCancel.TabIndex = 4;
|
||||
btnCancel.Text = "Abbrechen";
|
||||
btnCancel.UseVisualStyleBackColor = false;
|
||||
btnCancel.DialogResult = DialogResult.Cancel;
|
||||
//
|
||||
// frm_CreateInstance
|
||||
//
|
||||
AcceptButton = btnOk;
|
||||
BackColor = Color.FromArgb(45, 45, 45);
|
||||
CancelButton = btnCancel;
|
||||
ClientSize = new Size(430, 180);
|
||||
Controls.Add(lblInfo);
|
||||
Controls.Add(txtName);
|
||||
Controls.Add(lblPreview);
|
||||
Controls.Add(btnOk);
|
||||
Controls.Add(btnCancel);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "frm_CreateInstance";
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Neue Instanz erstellen";
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Label lblInfo;
|
||||
private TextBox txtName;
|
||||
private Label lblPreview;
|
||||
private Button btnOk;
|
||||
private Button btnCancel;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using ClawdDotNet.Services;
|
||||
|
||||
namespace ClawdDotNet;
|
||||
|
||||
public sealed partial class frm_CreateInstance : Form
|
||||
{
|
||||
public string InstanceName => txtName.Text.Trim();
|
||||
|
||||
public frm_CreateInstance()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnTxtNameTextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var name = txtName.Text.Trim();
|
||||
lblPreview.Text = string.IsNullOrWhiteSpace(name)
|
||||
? "Ordner: Instance-..."
|
||||
: $"Ordner: {InstanceDirectoryManager.BuildInstanceFolderName(name)}";
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,117 +0,0 @@
|
||||
namespace ClawdDotNet
|
||||
{
|
||||
partial class frm_InstanceManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frm_InstanceManager));
|
||||
toolStrip1 = new ToolStrip();
|
||||
btn_startInstance = new ToolStripButton();
|
||||
toolStripSeparator1 = new ToolStripSeparator();
|
||||
btn_createInstance = new ToolStripButton();
|
||||
dgv_instances = new DataGridView();
|
||||
notifyIcon1 = new NotifyIcon(components);
|
||||
toolStrip1.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)dgv_instances).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// toolStrip1
|
||||
//
|
||||
toolStrip1.ImageScalingSize = new Size(24, 24);
|
||||
toolStrip1.Items.AddRange(new ToolStripItem[] { btn_startInstance, toolStripSeparator1, btn_createInstance });
|
||||
toolStrip1.Location = new Point(0, 0);
|
||||
toolStrip1.Name = "toolStrip1";
|
||||
toolStrip1.Size = new Size(783, 34);
|
||||
toolStrip1.TabIndex = 0;
|
||||
toolStrip1.Text = "toolStrip1";
|
||||
//
|
||||
// btn_startInstance
|
||||
//
|
||||
btn_startInstance.Image = Properties.Resources.server_go;
|
||||
btn_startInstance.ImageTransparentColor = Color.Magenta;
|
||||
btn_startInstance.Name = "btn_startInstance";
|
||||
btn_startInstance.Size = new Size(146, 29);
|
||||
btn_startInstance.Text = "Start Instance";
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(6, 34);
|
||||
//
|
||||
// btn_createInstance
|
||||
//
|
||||
btn_createInstance.Alignment = ToolStripItemAlignment.Right;
|
||||
btn_createInstance.Image = Properties.Resources.server_add;
|
||||
btn_createInstance.ImageTransparentColor = Color.Magenta;
|
||||
btn_createInstance.Name = "btn_createInstance";
|
||||
btn_createInstance.Size = new Size(200, 29);
|
||||
btn_createInstance.Text = "Create New Instance";
|
||||
//
|
||||
// dgv_instances
|
||||
//
|
||||
dgv_instances.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
dgv_instances.Dock = DockStyle.Bottom;
|
||||
dgv_instances.Location = new Point(0, 45);
|
||||
dgv_instances.Name = "dgv_instances";
|
||||
dgv_instances.RowHeadersWidth = 62;
|
||||
dgv_instances.Size = new Size(783, 408);
|
||||
dgv_instances.TabIndex = 1;
|
||||
//
|
||||
// notifyIcon1
|
||||
//
|
||||
notifyIcon1.Text = "notifyIcon1";
|
||||
notifyIcon1.Visible = true;
|
||||
//
|
||||
// frm_InstanceManager
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(10F, 25F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(783, 453);
|
||||
Controls.Add(dgv_instances);
|
||||
Controls.Add(toolStrip1);
|
||||
HelpButton = true;
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
Name = "frm_InstanceManager";
|
||||
Text = "ClawdDotNet - Instance Manager";
|
||||
toolStrip1.ResumeLayout(false);
|
||||
toolStrip1.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)dgv_instances).EndInit();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private ToolStrip toolStrip1;
|
||||
private DataGridView dgv_instances;
|
||||
private ToolStripButton btn_createInstance;
|
||||
private ToolStripSeparator toolStripSeparator1;
|
||||
private ToolStripButton btn_startInstance;
|
||||
private NotifyIcon notifyIcon1;
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
using ClawdDotNet.Models;
|
||||
using ClawdDotNet.Services;
|
||||
|
||||
namespace ClawdDotNet;
|
||||
|
||||
public partial class frm_InstanceManager : Form
|
||||
{
|
||||
private readonly InstanceDirectoryManager _dirManager;
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly BindingSource _bindingSource = new();
|
||||
|
||||
/// <summary>
|
||||
/// Wird gesetzt, wenn der Benutzer eine Instanz zum Starten ausgewählt hat.
|
||||
/// Program.cs liest diesen Wert nach DialogResult.OK aus.
|
||||
/// </summary>
|
||||
public string? SelectedInstancePath { get; private set; }
|
||||
|
||||
public frm_InstanceManager(InstanceDirectoryManager dirManager, SettingsManager settingsManager)
|
||||
{
|
||||
_dirManager = dirManager;
|
||||
_settingsManager = settingsManager;
|
||||
|
||||
InitializeComponent();
|
||||
SetupForm();
|
||||
}
|
||||
|
||||
// Parameterloser Konstruktor für Designer
|
||||
public frm_InstanceManager()
|
||||
{
|
||||
_dirManager = new InstanceDirectoryManager("./Instances");
|
||||
_settingsManager = new SettingsManager();
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void SetupForm()
|
||||
{
|
||||
Text = "ClawdDotNet - Instance Manager";
|
||||
|
||||
// DataGridView konfigurieren
|
||||
dgv_instances.AutoGenerateColumns = false;
|
||||
dgv_instances.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||||
dgv_instances.MultiSelect = false;
|
||||
dgv_instances.AllowUserToAddRows = false;
|
||||
dgv_instances.AllowUserToDeleteRows = false;
|
||||
dgv_instances.ReadOnly = true;
|
||||
dgv_instances.BackgroundColor = Color.FromArgb(45, 45, 45);
|
||||
dgv_instances.DefaultCellStyle.BackColor = Color.FromArgb(55, 55, 55);
|
||||
dgv_instances.DefaultCellStyle.ForeColor = Color.White;
|
||||
dgv_instances.DefaultCellStyle.SelectionBackColor = Color.FromArgb(80, 120, 200);
|
||||
dgv_instances.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(35, 35, 35);
|
||||
dgv_instances.ColumnHeadersDefaultCellStyle.ForeColor = Color.White;
|
||||
dgv_instances.EnableHeadersVisualStyles = false;
|
||||
dgv_instances.Dock = DockStyle.Fill;
|
||||
|
||||
// Spalten
|
||||
dgv_instances.Columns.AddRange(
|
||||
new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = "InstanceName",
|
||||
HeaderText = "Instanzname",
|
||||
Width = 200
|
||||
},
|
||||
new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = "FolderName",
|
||||
HeaderText = "Ordner",
|
||||
Width = 200
|
||||
},
|
||||
new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = "AgentCount",
|
||||
HeaderText = "Agenten",
|
||||
Width = 80
|
||||
},
|
||||
new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = "ApiKeyStatus",
|
||||
HeaderText = "API-Key",
|
||||
Width = 120
|
||||
}
|
||||
);
|
||||
|
||||
dgv_instances.DataSource = _bindingSource;
|
||||
dgv_instances.DoubleClick += OnInstanceDoubleClick;
|
||||
|
||||
// Buttons verdrahten
|
||||
btn_startInstance.Click += OnStartInstanceClick;
|
||||
btn_createInstance.Click += OnCreateInstanceClick;
|
||||
|
||||
// Daten laden
|
||||
RefreshInstanceList();
|
||||
}
|
||||
|
||||
private void RefreshInstanceList()
|
||||
{
|
||||
var instances = _dirManager.ListInstances();
|
||||
_bindingSource.DataSource = instances;
|
||||
dgv_instances.Refresh();
|
||||
}
|
||||
|
||||
private void OnStartInstanceClick(object? sender, EventArgs e)
|
||||
{
|
||||
StartSelectedInstance();
|
||||
}
|
||||
|
||||
private void OnInstanceDoubleClick(object? sender, EventArgs e)
|
||||
{
|
||||
StartSelectedInstance();
|
||||
}
|
||||
|
||||
private void StartSelectedInstance()
|
||||
{
|
||||
var info = GetSelectedInstance();
|
||||
if (info is null)
|
||||
{
|
||||
MessageBox.Show("Bitte wähle eine Instanz aus.", "Hinweis",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedInstancePath = info.FolderPath;
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnCreateInstanceClick(object? sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new frm_CreateInstance();
|
||||
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var instanceName = dialog.InstanceName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(instanceName))
|
||||
{
|
||||
MessageBox.Show("Bitte gib einen Instanznamen ein.", "Fehler",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_dirManager.CreateInstance(instanceName);
|
||||
RefreshInstanceList();
|
||||
|
||||
MessageBox.Show(
|
||||
$"Instanz '{instanceName}' wurde erfolgreich erstellt.",
|
||||
"Instanz erstellt", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Fehler beim Erstellen der Instanz:\n{ex.Message}",
|
||||
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private InstanceInfo? GetSelectedInstance()
|
||||
{
|
||||
if (dgv_instances.SelectedRows.Count == 0)
|
||||
return null;
|
||||
|
||||
return dgv_instances.SelectedRows[0].DataBoundItem as InstanceInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
using ClawdDotNet.Core.Backup;
|
||||
|
||||
namespace ClawdDotNet;
|
||||
|
||||
/// <summary>
|
||||
/// Fragt ab, wohin wiederhergestellt wird und — falls nötig — die Passphrase.
|
||||
///
|
||||
/// Bewusst ein eigener Schritt statt stiller Vorbelegung: Eine Wiederherstellung
|
||||
/// überschreibt Arbeitsstände, das soll niemand versehentlich auslösen.
|
||||
/// </summary>
|
||||
public sealed class frm_RestoreBackup : Form
|
||||
{
|
||||
private readonly TextBox _txtTarget = new();
|
||||
private readonly TextBox _txtPassphrase = new() { UseSystemPasswordChar = true };
|
||||
private readonly CheckBox _chkOverwrite = new();
|
||||
|
||||
public string TargetDirectory => _txtTarget.Text.Trim();
|
||||
public string? Passphrase => _txtPassphrase.Text.Length > 0 ? _txtPassphrase.Text : null;
|
||||
public bool Overwrite => _chkOverwrite.Checked;
|
||||
|
||||
public frm_RestoreBackup(BackupManifest manifest, string suggestedTarget)
|
||||
{
|
||||
Text = "Sicherung wiederherstellen";
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
MinimizeBox = false;
|
||||
MaximizeBox = false;
|
||||
ClientSize = new Size(680, 380);
|
||||
|
||||
var layout = new TableLayoutPanel
|
||||
{
|
||||
ColumnCount = 3,
|
||||
Dock = DockStyle.Fill,
|
||||
Padding = new Padding(14),
|
||||
RowCount = 7
|
||||
};
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
for (var i = 0; i < 7; i++)
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
// ─── Beschreibung der Sicherung ───
|
||||
var info = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Margin = new Padding(0, 0, 0, 12),
|
||||
Text = $"Instanz: {manifest.InstanceName}\n"
|
||||
+ $"Erstellt: {manifest.CreatedAt:g}\n"
|
||||
+ $"Dateien: {manifest.Files.Count}\n"
|
||||
+ $"Zugangsdaten: {DescribeSecrets(manifest)}"
|
||||
};
|
||||
layout.Controls.Add(info, 0, 0);
|
||||
layout.SetColumnSpan(info, 3);
|
||||
|
||||
// ─── Zielordner ───
|
||||
layout.Controls.Add(new Label { Anchor = AnchorStyles.Left, AutoSize = true, Text = "Zielordner:" }, 0, 1);
|
||||
|
||||
_txtTarget.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
_txtTarget.Text = suggestedTarget;
|
||||
layout.Controls.Add(_txtTarget, 1, 1);
|
||||
|
||||
var browse = new Button { AutoSize = true, Text = "…" };
|
||||
browse.Click += OnBrowse;
|
||||
layout.Controls.Add(browse, 2, 1);
|
||||
|
||||
var targetHint = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
ForeColor = SystemColors.GrayText,
|
||||
Margin = new Padding(0, 2, 0, 10),
|
||||
MaximumSize = new Size(620, 0),
|
||||
Text = "Vorgeschlagen ist ein neuer Ordner. Die laufende Instanz zu überschreiben "
|
||||
+ "wird nicht empfohlen: Sie hält Dateien geöffnet und schreibt Teile davon "
|
||||
+ "sofort wieder zurück."
|
||||
};
|
||||
layout.Controls.Add(targetHint, 1, 2);
|
||||
layout.SetColumnSpan(targetHint, 2);
|
||||
|
||||
// ─── Passphrase ───
|
||||
var needsPassphrase = manifest.HasSecrets;
|
||||
|
||||
var lblPass = new Label
|
||||
{
|
||||
Anchor = AnchorStyles.Left,
|
||||
AutoSize = true,
|
||||
Enabled = needsPassphrase,
|
||||
Text = "Passphrase:"
|
||||
};
|
||||
layout.Controls.Add(lblPass, 0, 3);
|
||||
|
||||
_txtPassphrase.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
_txtPassphrase.Enabled = needsPassphrase;
|
||||
layout.Controls.Add(_txtPassphrase, 1, 3);
|
||||
|
||||
var passHint = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
ForeColor = SystemColors.GrayText,
|
||||
Margin = new Padding(0, 2, 0, 10),
|
||||
MaximumSize = new Size(620, 0),
|
||||
Text = needsPassphrase
|
||||
? "Diese Sicherung enthält geschützte Zugangsdaten."
|
||||
: "Diese Sicherung enthält keine Zugangsdaten — API-Schlüssel und Passwörter "
|
||||
+ "müssen nach dem Wiederherstellen neu eingetragen werden."
|
||||
};
|
||||
layout.Controls.Add(passHint, 1, 4);
|
||||
layout.SetColumnSpan(passHint, 2);
|
||||
|
||||
// ─── Überschreiben ───
|
||||
_chkOverwrite.AutoSize = true;
|
||||
_chkOverwrite.Margin = new Padding(0, 0, 0, 12);
|
||||
_chkOverwrite.Text = "Vorhandene Dateien im Zielordner überschreiben";
|
||||
layout.Controls.Add(_chkOverwrite, 1, 5);
|
||||
layout.SetColumnSpan(_chkOverwrite, 2);
|
||||
|
||||
// ─── Schaltflächen ───
|
||||
var buttons = new FlowLayoutPanel
|
||||
{
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.RightToLeft
|
||||
};
|
||||
|
||||
var cancel = new Button { AutoSize = true, DialogResult = DialogResult.Cancel, Text = "Abbrechen" };
|
||||
var ok = new Button { AutoSize = true, Text = "Wiederherstellen" };
|
||||
ok.Click += OnConfirm;
|
||||
|
||||
buttons.Controls.Add(cancel);
|
||||
buttons.Controls.Add(ok);
|
||||
layout.Controls.Add(buttons, 1, 6);
|
||||
layout.SetColumnSpan(buttons, 2);
|
||||
|
||||
Controls.Add(layout);
|
||||
AcceptButton = ok;
|
||||
CancelButton = cancel;
|
||||
}
|
||||
|
||||
private static string DescribeSecrets(BackupManifest manifest) => manifest.HasSecrets
|
||||
? $"{manifest.SecretCount}, mit Passphrase geschützt"
|
||||
: manifest.SecretCount > 0
|
||||
? $"{manifest.SecretCount} entfernt"
|
||||
: "keine";
|
||||
|
||||
private void OnBrowse(object? sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new FolderBrowserDialog
|
||||
{
|
||||
Description = "Zielordner für die Wiederherstellung",
|
||||
UseDescriptionForTitle = true
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
_txtTarget.Text = dialog.SelectedPath;
|
||||
}
|
||||
|
||||
private void OnConfirm(object? sender, EventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(TargetDirectory))
|
||||
{
|
||||
MessageBox.Show(this, "Bitte einen Zielordner angeben.",
|
||||
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_txtPassphrase.Enabled && string.IsNullOrEmpty(_txtPassphrase.Text))
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"Diese Sicherung enthält geschützte Zugangsdaten. Ohne Passphrase lässt sie " +
|
||||
"sich nicht wiederherstellen.",
|
||||
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
_txtPassphrase.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
namespace ClawdDotNet
|
||||
{
|
||||
partial class frm_chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frm_chat));
|
||||
webView_chat2 = new Microsoft.Web.WebView2.WinForms.WebView2();
|
||||
((System.ComponentModel.ISupportInitialize)webView_chat2).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// webView_chat2
|
||||
//
|
||||
webView_chat2.AllowExternalDrop = true;
|
||||
webView_chat2.CreationProperties = null;
|
||||
webView_chat2.DefaultBackgroundColor = Color.White;
|
||||
webView_chat2.Dock = DockStyle.Fill;
|
||||
webView_chat2.Location = new Point(0, 0);
|
||||
webView_chat2.Name = "webView_chat2";
|
||||
webView_chat2.Size = new Size(1003, 706);
|
||||
webView_chat2.TabIndex = 0;
|
||||
webView_chat2.ZoomFactor = 1D;
|
||||
//
|
||||
// frm_chat
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(10F, 25F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(1003, 706);
|
||||
Controls.Add(webView_chat2);
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
Name = "frm_chat";
|
||||
Text = "ClawdDotNet - Chat: AgentName";
|
||||
((System.ComponentModel.ISupportInitialize)webView_chat2).EndInit();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Microsoft.Web.WebView2.WinForms.WebView2 webView_chat2;
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using ClawdDotNet.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace ClawdDotNet;
|
||||
|
||||
public partial class frm_chat : Form
|
||||
{
|
||||
private readonly AgentConfig _agentConfig;
|
||||
private readonly AgentEngine _engine;
|
||||
private readonly string _instanceId;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private WebViewBridge? _bridge;
|
||||
|
||||
public frm_chat(AgentConfig agentConfig, AgentEngine engine,
|
||||
string instanceId, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_agentConfig = agentConfig;
|
||||
_engine = engine;
|
||||
_instanceId = instanceId;
|
||||
_logger = loggerFactory.CreateLogger($"ClawdDotNet.UI.Chat.{agentConfig.AgentId}");
|
||||
|
||||
InitializeComponent();
|
||||
Text = $"Chat – {agentConfig.DisplayName}";
|
||||
Load += async (_, _) => await InitWebViewAsync();
|
||||
}
|
||||
|
||||
public frm_chat()
|
||||
{
|
||||
_agentConfig = new AgentConfig();
|
||||
_engine = null!;
|
||||
_instanceId = "";
|
||||
_logger = LoggerFactory.Create(_ => { }).CreateLogger("Design");
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async Task InitWebViewAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await webView_chat2.EnsureCoreWebView2Async();
|
||||
|
||||
var uiPath = EmbeddedUiManager.GetExtractedPath();
|
||||
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
|
||||
"ui.clwd.internal", uiPath,
|
||||
CoreWebView2HostResourceAccessKind.DenyCors);
|
||||
|
||||
_bridge = new WebViewBridge(webView_chat2, _logger);
|
||||
_bridge.MessageReceived += OnBridgeMessage;
|
||||
|
||||
webView_chat2.CoreWebView2.Navigate(
|
||||
$"https://ui.clwd.internal/chat.html?agent={_agentConfig.AgentId}");
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
await _bridge.SendAsync(new BridgeMessage(
|
||||
Type: BridgeTypes.AgentListUpdate,
|
||||
AgentId: _agentConfig.AgentId,
|
||||
Extra: new
|
||||
{
|
||||
agents = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
agentId = _agentConfig.AgentId,
|
||||
displayName = _agentConfig.DisplayName,
|
||||
model = _agentConfig.Model
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
await LoadChatHistoryAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "frm_chat WebView2 init failed");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnBridgeMessage(BridgeMessage msg)
|
||||
{
|
||||
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
|
||||
|
||||
switch (msg.Type)
|
||||
{
|
||||
case BridgeTypes.UserMessage:
|
||||
if (msg.Content is not null)
|
||||
await HandleUserMessageAsync(msg.Content);
|
||||
break;
|
||||
|
||||
case BridgeTypes.RunNow:
|
||||
await HandleUserMessageAsync("Führe deine zugewiesenen Aufgaben aus.");
|
||||
break;
|
||||
|
||||
case BridgeTypes.AbortRun:
|
||||
_engine.AbortChat(_agentConfig.AgentId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleUserMessageAsync(string text)
|
||||
{
|
||||
if (_bridge is null) return;
|
||||
|
||||
await _bridge.SendAsync(new BridgeMessage(
|
||||
Type: BridgeTypes.ChatMessage,
|
||||
AgentId: _agentConfig.AgentId,
|
||||
Content: text,
|
||||
Extra: new { role = "user", timestamp = DateTime.Now }));
|
||||
|
||||
await _bridge.SendAsync(new BridgeMessage(
|
||||
Type: BridgeTypes.ChatTyping,
|
||||
AgentId: _agentConfig.AgentId));
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _engine.ChatAsync(
|
||||
_agentConfig, text, _instanceId, CancellationToken.None,
|
||||
source: ChatSource.WebView);
|
||||
|
||||
await _bridge.SendAsync(new BridgeMessage(
|
||||
Type: BridgeTypes.ChatMessage,
|
||||
AgentId: _agentConfig.AgentId,
|
||||
Content: result.FinalMessage ?? "[Keine Antwort]",
|
||||
Extra: new { role = "assistant", timestamp = DateTime.Now }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Chat failed");
|
||||
await _bridge!.SendAsync(new BridgeMessage(
|
||||
Type: BridgeTypes.ChatMessage,
|
||||
AgentId: _agentConfig.AgentId,
|
||||
Content: $"[Fehler: {ex.Message}]",
|
||||
Extra: new { role = "assistant", timestamp = DateTime.Now }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadChatHistoryAsync()
|
||||
{
|
||||
if (_bridge is null) return;
|
||||
|
||||
var history = _engine.GetChatHistory(_agentConfig.AgentId);
|
||||
await _bridge.SendAsync(new BridgeMessage(
|
||||
Type: BridgeTypes.ChatHistory,
|
||||
AgentId: _agentConfig.AgentId,
|
||||
Extra: history.Select(e => new
|
||||
{
|
||||
role = e.Role,
|
||||
content = e.Content,
|
||||
timestamp = e.Timestamp
|
||||
}).ToArray()));
|
||||
}
|
||||
|
||||
protected override void OnFormClosed(FormClosedEventArgs e)
|
||||
{
|
||||
_bridge?.Dispose();
|
||||
base.OnFormClosed(e);
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="splitContainer1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="splitContainer1.Location" type="System.Drawing.Point, System.Drawing">
|
||||
<value>3, 3</value>
|
||||
</data>
|
||||
<data name="splitContainer1.Orientation" type="System.Windows.Forms.Orientation, System.Windows.Forms">
|
||||
<value>Horizontal</value>
|
||||
</data>
|
||||
<data name="toolStrip_agentSettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1884, 25</value>
|
||||
</data>
|
||||
<data name="toolStrip_agentSettings.Text" xml:space="preserve">
|
||||
<value>toolStrip2</value>
|
||||
</data>
|
||||
<data name="dgv_agentlist.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Bottom</value>
|
||||
</data>
|
||||
<data name="dgv_agentlist.Location" type="System.Drawing.Point, System.Drawing">
|
||||
<value>0, 26</value>
|
||||
</data>
|
||||
<data name="dgv_agentlist.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1884, 310</value>
|
||||
</data>
|
||||
<data name="pg_agentsettings.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="pg_agentsettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1884, 547</value>
|
||||
</data>
|
||||
<data name="splitContainer1.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1884, 887</value>
|
||||
</data>
|
||||
<assembly alias="mscorlib" name="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="splitContainer1.SplitterDistance" type="System.Int32, mscorlib">
|
||||
<value>336</value>
|
||||
</data>
|
||||
<data name="splitContainer1.TabIndex" type="System.Int32, mscorlib">
|
||||
<value>0</value>
|
||||
</data>
|
||||
<data name="btn_showInstanceManager.Text" xml:space="preserve">
|
||||
<value>Show Instance Manager</value>
|
||||
</data>
|
||||
<data name="instanceToolStripMenuItem.Text" xml:space="preserve">
|
||||
<value>Instances</value>
|
||||
</data>
|
||||
<data name="label_openRouterStatus.Text" xml:space="preserve">
|
||||
<value>toolStripStatusLabel1</value>
|
||||
</data>
|
||||
<data name="toolStripStatusLabel1.Text" xml:space="preserve">
|
||||
<value>|</value>
|
||||
</data>
|
||||
<data name="label_openRouterCredits.Text" xml:space="preserve">
|
||||
<value>toolStripStatusLabel2</value>
|
||||
</data>
|
||||
<data name="statusStrip1.Location" type="System.Drawing.Point, System.Drawing">
|
||||
<value>0, 992</value>
|
||||
</data>
|
||||
<data name="statusStrip1.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1898, 32</value>
|
||||
</data>
|
||||
<data name="tabControl1.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
|
||||
<value>Top, Left</value>
|
||||
</data>
|
||||
<data name="webView_chat.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="webView_chat.Location" type="System.Drawing.Point, System.Drawing">
|
||||
<value>3, 3</value>
|
||||
</data>
|
||||
<data name="webView_chat.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1884, 887</value>
|
||||
</data>
|
||||
<data name="tabPage_Chat.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1890, 893</value>
|
||||
</data>
|
||||
<data name="rtb_log.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="rtb_log.Location" type="System.Drawing.Point, System.Drawing">
|
||||
<value>0, 34</value>
|
||||
</data>
|
||||
<data name="rtb_log.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1890, 859</value>
|
||||
</data>
|
||||
<data name="label_InfoLogModule.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>77, 29</value>
|
||||
</data>
|
||||
<data name="label_InfoLogModule.Text" xml:space="preserve">
|
||||
<value>Module:</value>
|
||||
</data>
|
||||
<data name="cb_LogModule.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>221, 34</value>
|
||||
</data>
|
||||
<data name="toolStripSeparator1.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>6, 34</value>
|
||||
</data>
|
||||
<data name="label_InfoLogLevel.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>85, 29</value>
|
||||
</data>
|
||||
<data name="label_InfoLogLevel.Text" xml:space="preserve">
|
||||
<value>LogLevel:</value>
|
||||
</data>
|
||||
<data name="cb_LogLevel.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>220, 34</value>
|
||||
</data>
|
||||
<data name="btn_logfolder.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAEESURBVEhL3ZKvDoJQFId5Dk0238GGr2DXbqBa3Cw6uy+g
|
||||
RZozaTM4gxvZQHA6kc05/2CAetzP7bIrF0Hk3iLbx91O+L5xLloQBKQSDS/TNMkwDKnAGQYw0HVdKnAK
|
||||
Ae/+kIK0wPV0JHvWI3veo4M1kR+wBjot24UQFpEW4OUAX6I2MJMc2K2GoRzrwp18HXDOHjUXp9cZFaeR
|
||||
GoC0OnWpODq8zqyRxMDGvYdyRtbIx8B6f6Py2HmT/xKJDVRqjY/ytAhm/FwI1FtdKg23gjCOaITdFz8X
|
||||
An3rIoiSYDL+Z2BzrDl3gMmiPwPAmrHu3IEksG6sXVkAwPlHAZVovu93VKKpfp4ISreGcqlKAwAAAABJ
|
||||
RU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btn_logfolder.ImageTransparentColor" type="System.Drawing.Color, System.Drawing">
|
||||
<value>Magenta</value>
|
||||
</data>
|
||||
<data name="btn_logfolder.Text" xml:space="preserve">
|
||||
<value>Open LogFolder</value>
|
||||
</data>
|
||||
<data name="toolStrip3.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1890, 34</value>
|
||||
</data>
|
||||
<data name="toolStrip3.Text" xml:space="preserve">
|
||||
<value>toolStrip3</value>
|
||||
</data>
|
||||
<data name="tabPage_Log.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1890, 893</value>
|
||||
</data>
|
||||
<data name="pg_appsettings.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="pg_appsettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1870, 843</value>
|
||||
</data>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="tabPage_appsettings.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
|
||||
<value>3, 3, 3, 3</value>
|
||||
</data>
|
||||
<data name="tabPage_appsettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1876, 849</value>
|
||||
</data>
|
||||
<data name="tabPage_appsettings.Text" xml:space="preserve">
|
||||
<value>General Settings</value>
|
||||
</data>
|
||||
<data name="pg_instancesettings.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="pg_instancesettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1870, 843</value>
|
||||
</data>
|
||||
<data name="tabPage_instanceettings.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
|
||||
<value>3, 3, 3, 3</value>
|
||||
</data>
|
||||
<data name="tabPage_instanceettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1876, 849</value>
|
||||
</data>
|
||||
<data name="tabPage_instanceettings.Text" xml:space="preserve">
|
||||
<value>Instance Settings</value>
|
||||
</data>
|
||||
<data name="tabControl2.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="tabControl2.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1884, 887</value>
|
||||
</data>
|
||||
<data name="tabPage_GeneralSettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1890, 893</value>
|
||||
</data>
|
||||
<data name="tabPage_AgentSettings.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1890, 893</value>
|
||||
</data>
|
||||
<data name="tabControl1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
|
||||
<value>Bottom</value>
|
||||
</data>
|
||||
<data name="tabControl1.Size" type="System.Drawing.Size, System.Drawing">
|
||||
<value>1898, 931</value>
|
||||
</data>
|
||||
<data name="$this.Text" xml:space="preserve">
|
||||
<value>ClawdDotNet - InstanceName</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -67,6 +67,13 @@ public sealed class AppHost : IAsyncDisposable
|
||||
public TelegramClientManager? TelegramClient { get; private init; }
|
||||
public OpenRouterStatusService? Status { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Die taegliche Sicherung. Immer gesetzt — ob sie etwas tut, entscheidet
|
||||
/// <c>AutoBackupEnabled</c> bei jedem Durchlauf neu, damit eine Aenderung in den
|
||||
/// Einstellungen ohne Neustart greift.
|
||||
/// </summary>
|
||||
public BackupScheduler? Backups { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Anbindung ans Deploymentcenter (Heartbeat, Fehler-Stream, Bugtracker, Updates).
|
||||
/// Null, wenn Adresse oder Token fehlen.
|
||||
@@ -211,6 +218,16 @@ public sealed class AppHost : IAsyncDisposable
|
||||
await host.ConnectTelegramAsync(callbacks, logger);
|
||||
await host.StartDeploymentcenterAsync(loggerFactory, ct);
|
||||
|
||||
// Die Oberflaeche bietet die taegliche Sicherung an und schreibt Uhrzeit und
|
||||
// Zielordner in die Einstellungen — gestartet wurde der Takt dazu bisher nicht,
|
||||
// die Einstellung lief also ins Leere.
|
||||
host.Backups = new BackupScheduler(
|
||||
instancePath, instance.InstanceName, settings,
|
||||
loggerFactory.CreateLogger("ClawdDotNet.Backup"));
|
||||
|
||||
host.Backups.Start();
|
||||
host._shutdown.Add(host.Backups.DisposeAsync);
|
||||
|
||||
return new StartupResult(host, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
|
||||
<!-- Kein Aufruf im Code: Microsoft.Data.Sqlite bringt das Buendel selbst mit, aber
|
||||
in 2.1.11 - und darin steckt die SQLite-Fassung mit GHSA-2m69-gcr7-jv3q
|
||||
(NU1903, hoch). Der direkte Verweis hebt core, provider und die native
|
||||
Bibliothek gemeinsam auf 2.1.13 an; einzeln angehoben liefen die drei
|
||||
auseinander. Faellt weg, sobald Microsoft.Data.Sqlite selbst nachzieht. -->
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.13" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||
|
||||
@@ -340,12 +340,3 @@ public abstract partial class PageViewModel(string title) : ViewModelBase
|
||||
{
|
||||
public string Title { get; } = title;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platzhalter, solange ein Bereich noch nicht portiert ist.
|
||||
/// </summary>
|
||||
public sealed partial class PlaceholderPageViewModel(string title, string note)
|
||||
: PageViewModel(title)
|
||||
{
|
||||
public string Note { get; } = note;
|
||||
}
|
||||
|
||||
@@ -37,19 +37,12 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
[ObservableProperty]
|
||||
private int _statusCheckIntervalSeconds = 60;
|
||||
|
||||
// ─── Backup ───
|
||||
[ObservableProperty]
|
||||
private string _backupDirectory = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _autoBackupEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _autoBackupTime = "03:00";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _backupKeepCount = 14;
|
||||
|
||||
// Die Sicherungs-Einstellungen (Zielordner, Uhrzeit, Anzahl) stehen bewusst
|
||||
// NICHT hier, sondern allein auf der Sicherungs-Seite. Sie lagen frueher
|
||||
// doppelt vor: Die Felder waren aus der Ansicht bereits verschwunden, wurden
|
||||
// aber weiter beim Start gelesen und beim Speichern zurueckgeschrieben - wer
|
||||
// die Uhrzeit auf der Sicherungs-Seite aenderte und danach hier speicherte,
|
||||
// bekam den alten Wert zurueck.
|
||||
// ─── Deploymentcenter ───
|
||||
[ObservableProperty]
|
||||
private string _deploymentcenterUrl = "https://dc.mhdf.de";
|
||||
@@ -123,10 +116,6 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
LogRefreshIntervalMs = s.LogRefreshIntervalMs;
|
||||
OpenRouterBaseUrl = s.OpenRouterBaseUrl;
|
||||
StatusCheckIntervalSeconds = s.StatusCheckIntervalSeconds;
|
||||
BackupDirectory = s.BackupDirectory;
|
||||
AutoBackupEnabled = s.AutoBackupEnabled;
|
||||
AutoBackupTime = s.AutoBackupTime;
|
||||
BackupKeepCount = s.BackupKeepCount;
|
||||
DeploymentcenterUrl = s.DeploymentcenterUrl;
|
||||
DeploymentcenterToken = s.DeploymentcenterToken;
|
||||
DeploymentcenterEnvironment = s.DeploymentcenterEnvironment;
|
||||
@@ -155,10 +144,6 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
s.LogRefreshIntervalMs = LogRefreshIntervalMs;
|
||||
s.OpenRouterBaseUrl = OpenRouterBaseUrl.Trim();
|
||||
s.StatusCheckIntervalSeconds = StatusCheckIntervalSeconds;
|
||||
s.BackupDirectory = BackupDirectory.Trim();
|
||||
s.AutoBackupEnabled = AutoBackupEnabled;
|
||||
s.AutoBackupTime = AutoBackupTime.Trim();
|
||||
s.BackupKeepCount = BackupKeepCount;
|
||||
s.DeploymentcenterUrl = DeploymentcenterUrl.Trim();
|
||||
s.DeploymentcenterToken = DeploymentcenterToken.Trim();
|
||||
s.DeploymentcenterEnvironment = DeploymentcenterEnvironment.Trim();
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="400"
|
||||
x:Class="ClawdDotNet.Desktop.Views.PlaceholderPageView"
|
||||
x:DataType="vm:PlaceholderPageViewModel">
|
||||
|
||||
<Border Classes="card" Margin="16" VerticalAlignment="Top">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="heading" Text="{Binding Title}" />
|
||||
<TextBlock Classes="caption" Text="{Binding Note}" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="caption" Margin="0,8,0,0"
|
||||
Text="Dieser Bereich ist noch nicht portiert." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,8 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class PlaceholderPageView : UserControl
|
||||
{
|
||||
public PlaceholderPageView() => InitializeComponent();
|
||||
}
|
||||
@@ -11,8 +11,16 @@
|
||||
<PackageReference Include="Npgsql" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.8.1" />
|
||||
|
||||
<!-- Kein Aufruf im Code, trotzdem noetig: MongoDB.Driver zieht SharpCompress in
|
||||
0.30.1 herein, und diese Fassung hat eine bekannte Luecke (NU1902,
|
||||
GHSA-6c8g-7p36-r338). Der direkte Verweis hebt sie an. Beim Aufraeumen am
|
||||
2026-08-23 einmal entfernt und sofort wieder eingesetzt: Ohne ihn meldet
|
||||
die Pruefung auf verwundbare Pakete das alte Paket zurueck.
|
||||
|
||||
Snappier stand daneben und ist ersatzlos entfallen: Der Treiber loest es
|
||||
ohnehin auf dieselbe Fassung 1.3.1 auf, der Verweis hob also nichts an. -->
|
||||
<PackageReference Include="SharpCompress" Version="0.48.0" />
|
||||
<PackageReference Include="Snappier" Version="1.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||