Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
433 lines
15 KiB
C#
433 lines
15 KiB
C#
using System.Text.Json;
|
|
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Models;
|
|
|
|
namespace ClawdDotNet.Services;
|
|
|
|
/// <summary>
|
|
/// Verwaltet die gesamte Verzeichnisstruktur für Instanzen und Agenten.
|
|
///
|
|
/// Layout:
|
|
/// {InstancesDir}/
|
|
/// ├── Instance-{Name}/
|
|
/// │ ├── InstanceSettings.json
|
|
/// │ ├── TokenUsage.json
|
|
/// │ └── Agents/
|
|
/// │ ├── AgentList.json
|
|
/// │ └── Agent-{Name}/
|
|
/// │ ├── AgentSettings.json
|
|
/// │ ├── Soul.md
|
|
/// │ ├── Identity.md
|
|
/// │ ├── Logs/
|
|
/// │ └── Workspace/
|
|
/// </summary>
|
|
public sealed class InstanceDirectoryManager
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
private readonly string _instancesDir;
|
|
private readonly Lock _tokenUsageLock = new();
|
|
|
|
public InstanceDirectoryManager(string instancesDirectory)
|
|
{
|
|
_instancesDir = Path.GetFullPath(instancesDirectory);
|
|
Directory.CreateDirectory(_instancesDir);
|
|
}
|
|
|
|
public string InstancesDirectory => _instancesDir;
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// INSTANZ-OPERATIONEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public static string BuildInstanceFolderName(string instanceName)
|
|
=> $"Instance-{SanitizeName(instanceName)}";
|
|
|
|
public string GetInstancePath(string instanceName)
|
|
=> Path.Combine(_instancesDir, BuildInstanceFolderName(instanceName));
|
|
|
|
public List<InstanceInfo> ListInstances()
|
|
{
|
|
var result = new List<InstanceInfo>();
|
|
|
|
if (!Directory.Exists(_instancesDir))
|
|
return result;
|
|
|
|
foreach (var dir in Directory.GetDirectories(_instancesDir, "Instance-*"))
|
|
{
|
|
var folderName = Path.GetFileName(dir);
|
|
var settingsPath = Path.Combine(dir, "InstanceSettings.json");
|
|
|
|
var info = new InstanceInfo
|
|
{
|
|
FolderName = folderName,
|
|
FolderPath = dir,
|
|
InstanceName = folderName.Replace("Instance-", "")
|
|
};
|
|
|
|
if (File.Exists(settingsPath))
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(settingsPath);
|
|
var config = JsonSerializer.Deserialize<InstanceConfig>(json, JsonOpts);
|
|
if (config is not null)
|
|
{
|
|
info.InstanceName = config.InstanceName;
|
|
info.ApiKeyStatus = string.IsNullOrWhiteSpace(config.OpenRouterApiKey)
|
|
? "Fehlt" : "Konfiguriert";
|
|
}
|
|
}
|
|
catch { /* defekte Config → Standardwerte */ }
|
|
}
|
|
|
|
// Agenten zählen
|
|
var agentsDir = Path.Combine(dir, "Agents");
|
|
if (Directory.Exists(agentsDir))
|
|
info.AgentCount = Directory.GetDirectories(agentsDir, "Agent-*").Length;
|
|
|
|
result.Add(info);
|
|
}
|
|
|
|
return result.OrderBy(i => i.InstanceName).ToList();
|
|
}
|
|
|
|
public string CreateInstance(string instanceName)
|
|
{
|
|
var instanceDir = GetInstancePath(instanceName);
|
|
|
|
if (Directory.Exists(instanceDir))
|
|
throw new InvalidOperationException($"Instanz '{instanceName}' existiert bereits.");
|
|
|
|
// Hauptverzeichnis
|
|
Directory.CreateDirectory(instanceDir);
|
|
|
|
// Agents-Unterverzeichnis
|
|
var agentsDir = Path.Combine(instanceDir, "Agents");
|
|
Directory.CreateDirectory(agentsDir);
|
|
|
|
// SharedWorkspace-Verzeichnis
|
|
Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
|
|
// InstanceSettings.json
|
|
var config = new InstanceConfig
|
|
{
|
|
InstanceId = Guid.NewGuid().ToString("N")[..8],
|
|
InstanceName = instanceName,
|
|
LogDirectory = "./Logs",
|
|
WorkingDirectory = instanceDir
|
|
};
|
|
SaveJson(Path.Combine(instanceDir, "InstanceSettings.json"), config);
|
|
|
|
// TokenUsage.json (leer)
|
|
var tokenUsage = new TokenUsageFile
|
|
{
|
|
InstanceId = config.InstanceId,
|
|
InstanceName = instanceName
|
|
};
|
|
SaveJson(Path.Combine(instanceDir, "TokenUsage.json"), tokenUsage);
|
|
|
|
// AgentList.json (leer)
|
|
SaveJson(Path.Combine(agentsDir, "AgentList.json"), new AgentListFile());
|
|
|
|
return instanceDir;
|
|
}
|
|
|
|
public InstanceConfig LoadInstanceConfig(string instanceDir)
|
|
{
|
|
var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json");
|
|
|
|
if (!File.Exists(settingsPath))
|
|
throw new FileNotFoundException($"InstanceSettings.json nicht gefunden in: {instanceDir}");
|
|
|
|
var json = File.ReadAllText(settingsPath);
|
|
var config = JsonSerializer.Deserialize<InstanceConfig>(json, JsonOpts)
|
|
?? throw new InvalidOperationException("InstanceSettings.json ist leer oder ungültig.");
|
|
|
|
// Agenten aus Verzeichnisstruktur laden
|
|
config.Agents.Clear();
|
|
var agentsDir = Path.Combine(instanceDir, "Agents");
|
|
|
|
// Sicherstellen, dass SharedWorkspace existiert (Migration)
|
|
Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
|
|
// AgentList.json für Descriptions laden
|
|
var agentListPath = Path.Combine(agentsDir, "AgentList.json");
|
|
var agentList = File.Exists(agentListPath)
|
|
? LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile()
|
|
: new AgentListFile();
|
|
|
|
if (Directory.Exists(agentsDir))
|
|
{
|
|
foreach (var agentDir in Directory.GetDirectories(agentsDir, "Agent-*").OrderBy(d => d))
|
|
{
|
|
var agent = LoadAgentConfig(agentDir);
|
|
|
|
var folderName = Path.GetFileName(agentDir);
|
|
var listEntry = agentList.Agents.FirstOrDefault(a => a.FolderName == folderName);
|
|
if (listEntry is not null && !string.IsNullOrWhiteSpace(listEntry.Description))
|
|
agent.Description = listEntry.Description;
|
|
|
|
config.Agents.Add(agent);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
public void SaveInstanceConfig(string instanceDir, InstanceConfig config)
|
|
{
|
|
var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json");
|
|
SaveJson(settingsPath, config);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// AGENTEN-OPERATIONEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public static string BuildAgentFolderName(string agentName)
|
|
=> $"Agent-{SanitizeName(agentName)}";
|
|
|
|
public string GetAgentPath(string instanceDir, string agentName)
|
|
=> Path.Combine(instanceDir, "Agents", BuildAgentFolderName(agentName));
|
|
|
|
public string CreateAgent(string instanceDir, string agentName, string description = "")
|
|
{
|
|
var agentDir = GetAgentPath(instanceDir, agentName);
|
|
|
|
if (Directory.Exists(agentDir))
|
|
throw new InvalidOperationException($"Agent '{agentName}' existiert bereits in dieser Instanz.");
|
|
|
|
// Verzeichnisse anlegen
|
|
Directory.CreateDirectory(agentDir);
|
|
Directory.CreateDirectory(Path.Combine(agentDir, "Logs"));
|
|
Directory.CreateDirectory(Path.Combine(agentDir, "Workspace"));
|
|
|
|
// AgentSettings.json
|
|
var agentConfig = new AgentConfig
|
|
{
|
|
AgentId = SanitizeName(agentName).ToLowerInvariant(),
|
|
DisplayName = agentName,
|
|
Model = "anthropic/claude-sonnet-4-5"
|
|
};
|
|
SaveAgentSettings(agentDir, agentConfig);
|
|
|
|
// Identity.md
|
|
File.WriteAllText(Path.Combine(agentDir, "Identity.md"),
|
|
$"""
|
|
# Identity: {agentName}
|
|
|
|
Du bist **{agentName}**, ein spezialisierter KI-Agent im ClawdDotNet-System.
|
|
|
|
## Rolle
|
|
[Beschreibe hier die Rolle und Verantwortlichkeiten des Agenten]
|
|
|
|
## Expertise
|
|
[Beschreibe hier die Fachgebiete und Fähigkeiten]
|
|
|
|
## Kontext
|
|
[Beschreibe hier den Arbeitskontext und die Teamzugehörigkeit]
|
|
""");
|
|
|
|
// Soul.md
|
|
File.WriteAllText(Path.Combine(agentDir, "Soul.md"),
|
|
$"""
|
|
# Soul: {agentName}
|
|
|
|
## Persönlichkeit
|
|
- Gründlich und zuverlässig
|
|
- Klar und präzise in der Kommunikation
|
|
- Proaktiv bei der Problemerkennung
|
|
|
|
## Arbeitsweise
|
|
- Analysiere Aufgaben sorgfältig bevor du handelst
|
|
- Dokumentiere deine Entscheidungen und Ergebnisse
|
|
- Nutze die dir zugewiesenen Tools effizient
|
|
|
|
## Werte
|
|
- Genauigkeit vor Geschwindigkeit
|
|
- Transparenz in der Entscheidungsfindung
|
|
- Sicherheit und Datenschutz haben Priorität
|
|
""");
|
|
|
|
// AgentList.json aktualisieren
|
|
UpdateAgentList(instanceDir, agentName, description, BuildAgentFolderName(agentName));
|
|
|
|
return agentDir;
|
|
}
|
|
|
|
public AgentConfig LoadAgentConfig(string agentDir)
|
|
{
|
|
// AgentSettings.json laden
|
|
var settingsPath = Path.Combine(agentDir, "AgentSettings.json");
|
|
AgentConfig config;
|
|
|
|
if (File.Exists(settingsPath))
|
|
{
|
|
var json = File.ReadAllText(settingsPath);
|
|
config = JsonSerializer.Deserialize<AgentConfig>(json, JsonOpts) ?? new AgentConfig();
|
|
}
|
|
else
|
|
{
|
|
config = new AgentConfig
|
|
{
|
|
AgentId = Path.GetFileName(agentDir).Replace("Agent-", "").ToLowerInvariant(),
|
|
DisplayName = Path.GetFileName(agentDir).Replace("Agent-", "")
|
|
};
|
|
}
|
|
|
|
// Identity.md laden
|
|
var identityPath = Path.Combine(agentDir, "Identity.md");
|
|
if (File.Exists(identityPath))
|
|
config.Identity = File.ReadAllText(identityPath);
|
|
|
|
// Soul.md laden
|
|
var soulPath = Path.Combine(agentDir, "Soul.md");
|
|
if (File.Exists(soulPath))
|
|
config.Soul = File.ReadAllText(soulPath);
|
|
|
|
// Agent-Verzeichnis merken (stabil auch bei DisplayName-Änderungen)
|
|
config.AgentDir = Path.GetFullPath(agentDir);
|
|
|
|
// Workspace-Pfad setzen
|
|
config.WorkspacePath = Path.GetFullPath(Path.Combine(agentDir, "Workspace"));
|
|
|
|
// SharedWorkspace-Pfad setzen
|
|
var agentsDir = Path.GetDirectoryName(agentDir); // Dies ist der /Agents Ordner
|
|
if (agentsDir != null)
|
|
{
|
|
config.SharedWorkspacePath = Path.GetFullPath(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
public void SaveAgentSettings(string agentDir, AgentConfig config)
|
|
{
|
|
var settingsPath = Path.Combine(agentDir, "AgentSettings.json");
|
|
SaveJson(settingsPath, config);
|
|
}
|
|
|
|
public void SaveAgentIdentity(string agentDir, string identity)
|
|
{
|
|
File.WriteAllText(Path.Combine(agentDir, "Identity.md"), identity);
|
|
}
|
|
|
|
public void SaveAgentSoul(string agentDir, string soul)
|
|
{
|
|
File.WriteAllText(Path.Combine(agentDir, "Soul.md"), soul);
|
|
}
|
|
|
|
public void RemoveAgent(string instanceDir, string agentFolderName)
|
|
{
|
|
var agentDir = Path.Combine(instanceDir, "Agents", agentFolderName);
|
|
if (Directory.Exists(agentDir))
|
|
Directory.Delete(agentDir, recursive: true);
|
|
|
|
// AgentList.json aktualisieren
|
|
var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json");
|
|
if (File.Exists(agentListPath))
|
|
{
|
|
var list = LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile();
|
|
list.Agents.RemoveAll(a => a.FolderName == agentFolderName);
|
|
SaveJson(agentListPath, list);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// TOKEN USAGE
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public void AppendTokenUsage(string instanceDir, TokenUsageRecord record)
|
|
{
|
|
lock (_tokenUsageLock)
|
|
{
|
|
var path = Path.Combine(instanceDir, "TokenUsage.json");
|
|
TokenUsageFile file;
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
try
|
|
{
|
|
file = LoadJson<TokenUsageFile>(path) ?? new TokenUsageFile();
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Korrupte Datei: Backup erstellen, neu anfangen
|
|
var backupPath = path + $".corrupt_{DateTime.Now:yyyyMMdd_HHmmss}";
|
|
try { File.Copy(path, backupPath, overwrite: true); } catch { /* best effort */ }
|
|
file = new TokenUsageFile();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
file = new TokenUsageFile();
|
|
}
|
|
|
|
file.Records.Add(record);
|
|
SaveJson(path, file);
|
|
}
|
|
}
|
|
|
|
public TokenUsageFile LoadTokenUsage(string instanceDir)
|
|
{
|
|
var path = Path.Combine(instanceDir, "TokenUsage.json");
|
|
return File.Exists(path)
|
|
? LoadJson<TokenUsageFile>(path) ?? new TokenUsageFile()
|
|
: new TokenUsageFile();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// HILFSMETHODEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
private void UpdateAgentList(string instanceDir, string agentName, string description, string folderName)
|
|
{
|
|
var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json");
|
|
var list = File.Exists(agentListPath)
|
|
? LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile()
|
|
: new AgentListFile();
|
|
|
|
// Duplikat-Check
|
|
if (list.Agents.All(a => a.FolderName != folderName))
|
|
{
|
|
list.Agents.Add(new AgentListItem
|
|
{
|
|
Name = agentName,
|
|
Description = description,
|
|
FolderName = folderName
|
|
});
|
|
}
|
|
|
|
SaveJson(agentListPath, list);
|
|
}
|
|
|
|
private static void SaveJson<T>(string path, T obj)
|
|
{
|
|
var dir = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(dir))
|
|
Directory.CreateDirectory(dir);
|
|
|
|
var json = JsonSerializer.Serialize(obj, JsonOpts);
|
|
File.WriteAllText(path, json);
|
|
}
|
|
|
|
private static T? LoadJson<T>(string path)
|
|
{
|
|
var json = File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<T>(json, JsonOpts);
|
|
}
|
|
|
|
private static string SanitizeName(string name)
|
|
{
|
|
var sanitized = name.Trim();
|
|
foreach (var c in Path.GetInvalidFileNameChars())
|
|
sanitized = sanitized.Replace(c, '_');
|
|
return sanitized.Replace(' ', '_');
|
|
}
|
|
}
|