Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/views/agents.js
T
RichardandClaude Opus 5 a511480b77 Instanzmanager Stufe 4: eine Adresse, ein Zugang, alle Instanzen (#528, E-M5)
Der Managerprozess. ClawdDotNet.Instanzmanager ist eine Konsolenanwendung:
Lizenz pruefen, uebernehmen was laeuft, Weboberflaeche aufhaengen, warten.
Beim Herunterfahren beendet er keine Instanz.

Zwei neue Projekte. ClawdDotNet.Manager.Web traegt die Weboberflaeche -
ohne jeden Verweis auf ClawdDotNet.Web, denn dort haengt App und daran
sechzehn Werkzeugprojekte. ClawdDotNet.ManagerApp ist der Einstiegspunkt.
wwwroot ist aus der Instanz in den Manager gezogen.

DER SCHNITT DER HTTP-FLAECHE: Unter /api/verwaltung steht, was den Manager
angeht - auflisten, anlegen, starten, anhalten. Alles andere unter /api
geht unveraendert an eine Instanz. Er leitet weiter, er implementiert
nicht. Welche Instanz gemeint ist, sagt die Kopfzeile X-Instanz; laeuft
genau eine, ist sie gemeint, und laufen mehrere, wird nachgefragt statt
geraten - eine Anfrage an die falsche Instanz ist schlimmer als eine
abgelehnte.

Der zusammengefuehrte Ereignisstrom haelt je laufender Instanz eine
Zuleitung und schreibt an jedes Ereignis, von wem es kommt. Ergaenzt statt
verpackt: Ein Umschlag waere einfacher gewesen, haette aber jede Ansicht
der Weboberflaeche zum Umschreiben gezwungen.

ABNAHME gegen laufende Prozesse: 401 ohne Token, Oberflaeche ohne Token
200, Instanz ueber den Manager gestartet und angehalten, /api/info und
/api/status unverfaelscht durchgereicht, /events/logs mit
"instanz":"Instance-Test" an jeder Zeile. Manager abgeschossen: Instanz
laeuft weiter. Manager neu gestartet: "1 Instanzen bekannt, 1 laufen
bereits" - uebernommen. E-M6 damit in beide Richtungen belegt.

DREI DEFEKTE, die erst der Lauf gezeigt hat:

1. Der Manager hing an einer Konsolenabfrage nach dem Lizenzschluessel. Ein
   Test auf Console.IsInputRedirected reicht dafuer NICHT: Er beantwortet
   "ist die Eingabe umgeleitet", nicht "kann jemand antworten" - ein
   Prozess ganz ohne Konsole, der gefaehrlichste Fall, ist nicht
   umgeleitet. Der Manager fragt jetzt gar nicht.
2. Eine unlesbare manager.json wurde stillschweigend durch Vorgaben
   ersetzt: falsches Instanzverzeichnis, anderer Port, Lizenzfrage - und
   nichts sagte, woran es lag. LoadProblem meldet den Grund jetzt.
3. Der Instanzprozess erbte CLAWD_CONFIG_DIR vom Manager und legte sich in
   dessen Konfigurationsverzeichnis eine eigene Settings.json an. Eine
   Umgebungsvariable vererbt sich, ein Argument nicht - der Manager nimmt
   sein Verzeichnis jetzt ueber --config.

SharedLoggerProvider ist nach Core gezogen: Es gibt jetzt zwei Webhosts,
die dasselbe brauchen, und sie duerfen einander nicht kennen.

NOCH OFFEN in dieser Stufe: Die Weboberflaeche kennt noch keine Instanzen -
Instanzauswahl, X-Instanz und die Verwaltungsansicht sind Arbeit an
wwwroot. Und webServerPort steht noch in der Instanz.

Build ohne Warnungen, 775 Tests gruen, 8 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:19:49 +02:00

414 lines
12 KiB
JavaScript

// ClawdDotNet — Agenten-Verwaltung
//
// Übersicht, Anlegen, Löschen von Agenten, Werkzeug-Schalter und maskierte Einstellungen.
import { api } from "../modules/api.js";
import { abonnieren } from "../modules/events.js";
import {
erstelleElement,
erstelleIcon,
PFADE,
erstellePlakette,
erstelleKarte,
erstelleModal,
zeigeMeldung
} from "../modules/dom.js";
let gewaehlterAgentId = null;
let agenten = [];
let entladeEreignisse = [];
export async function rendereAgenten(parameter = {}) {
const container = document.getElementById("ansicht-inhalt");
if (!container) return;
container.replaceChildren();
entladeEreignisse.forEach((c) => c());
entladeEreignisse = [];
const layout = erstelleElement("div", { klasse: "agenten-ansicht" });
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
kopfZeile.append(
erstelleElement("h1", { text: "Agenten" }),
erstelleElement(
"button",
{
klasse: "schaltflaeche primaer",
onclick: oeffneNeuerAgentDialog
},
erstelleIcon(PFADE.plus, 16),
erstelleElement("span", { text: " Neuer Agent" })
)
);
const raster = erstelleElement("div", { klasse: "agenten-raster" });
const listeContainer = erstelleElement("div", { klasse: "agenten-liste-bereich" });
const detailContainer = erstelleElement("div", { klasse: "agenten-detail-bereich" });
raster.append(listeContainer, detailContainer);
layout.append(kopfZeile, raster);
container.append(layout);
await ladeUndRendereListe(parameter.agentId);
entladeEreignisse.push(
abonnieren("instance", (daten) => {
if (daten.kind === "Agents") {
ladeUndRendereListe(gewaehlterAgentId);
}
})
);
}
async function ladeUndRendereListe(zielId = null) {
const listeContainer = document.querySelector(".agenten-liste-bereich");
if (!listeContainer) return;
listeContainer.replaceChildren(
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Agenten…" })
);
try {
agenten = await api("/agents");
listeContainer.replaceChildren();
if (!agenten || agenten.length === 0) {
listeContainer.append(
erstelleElement("div", {
klasse: "hinweis-zentriert",
text: "Keine Agenten eingerichtet."
})
);
rendereDetails(null);
return;
}
const auswahlId = zielId && agenten.some((a) => a.agentId === zielId)
? zielId
: agenten[0].agentId;
for (const a of agenten) {
const eintrag = erstelleElement("div", {
klasse: `agenten-karte ${a.agentId === auswahlId ? "aktiv" : ""}`,
onclick: () => {
document.querySelectorAll(".agenten-karte").forEach((k) => k.classList.remove("aktiv"));
eintrag.classList.add("aktiv");
rendereDetails(a.agentId);
}
});
const top = erstelleElement("div", { klasse: "agent-karte-top" });
const name = erstelleElement("strong", { text: a.displayName });
const status = a.isRunning
? erstellePlakette("Beschäftigt", "warnung")
: erstellePlakette("Bereit", "neutral");
top.append(name, status);
const meta = erstelleElement("div", { klasse: "agent-karte-meta" });
meta.append(
erstelleElement("span", { klasse: "gedaempft", text: `ID: ${a.agentId}` }),
erstelleElement("span", { klasse: "gedaempft", text: ` · Modell: ${a.model}` })
);
const werkzeugeZaehler = a.tools ? a.tools.filter((t) => t.enabled).length : 0;
const werkzeugeText = erstelleElement("div", {
klasse: "agent-karte-werkzeuge gedaempft",
text: `${werkzeugeZaehler} Werkzeuge aktiv`
});
eintrag.append(top, meta, werkzeugeText);
listeContainer.append(eintrag);
}
rendereDetails(auswahlId);
} catch (err) {
listeContainer.replaceChildren(
erstelleElement("div", { klasse: "fehler-zentriert", text: err.message })
);
}
}
async function rendereDetails(agentId) {
gewaehlterAgentId = agentId;
const detailContainer = document.querySelector(".agenten-detail-bereich");
if (!detailContainer) return;
detailContainer.replaceChildren();
if (!agentId) {
detailContainer.append(
erstelleKarte("Agent-Details", [
erstelleElement("p", { klasse: "gedaempft", text: "Wähle links einen Agenten aus." })
])
);
return;
}
const agent = agenten.find((a) => a.agentId === agentId);
if (!agent) return;
// Kopf-Aktionen (Chatten, Löschen)
const aktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
const zumChatBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche flach",
title: "Mit diesem Agenten chatten",
onclick: () => {
window.location.hash = `#/chat/${encodeURIComponent(agent.agentId)}`;
}
},
erstelleIcon(PFADE.chat, 14),
erstelleElement("span", { text: " Chatten" })
);
const loeschenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche gefahr flach",
title: "Diesen Agenten löschen",
onclick: () => loescheAgent(agent.agentId, agent.displayName)
},
erstelleIcon(PFADE.muelleimer, 14),
erstelleElement("span", { text: " Löschen" })
);
aktionen.append(zumChatBtn, loeschenBtn);
// Stammdaten-Karte
const stammdatenListe = erstelleElement("dl", { klasse: "datenliste" });
const felder = [
["Name", agent.displayName],
["Kennung (ID)", agent.agentId],
["Modell", agent.model],
["Beschreibung", agent.description || "—"]
];
for (const [bez, wert] of felder) {
stammdatenListe.append(
erstelleElement("dt", { text: bez }),
erstelleElement("dd", { text: wert })
);
}
const basisKarte = erstelleKarte(agent.displayName, [stammdatenListe], { aktionen });
// Werkzeuge-Karte
const werkzeugeListe = erstelleElement("div", { klasse: "werkzeuge-liste" });
if (!agent.tools || agent.tools.length === 0) {
werkzeugeListe.append(
erstelleElement("p", { klasse: "gedaempft", text: "Keine Werkzeuge für diesen Agenten konfiguriert." })
);
} else {
for (const tool of agent.tools) {
const zeile = erstelleElement("div", { klasse: "werkzeug-zeile" });
const links = erstelleElement("div", { klasse: "werkzeug-info" });
const toolTitel = erstelleElement("strong", { text: tool.toolName });
const toolDesc = erstelleElement("div", {
klasse: "werkzeug-beschreibung gedaempft",
text: tool.description || ""
});
links.append(toolTitel, toolDesc);
const rechts = erstelleElement("div", { klasse: "werkzeug-aktionen" });
const einstellungenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche flach klein",
title: "Werkzeug-Einstellungen einsehen",
onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.toolName)
},
erstelleIcon(PFADE.einstellungen, 14),
erstelleElement("span", { text: " Konfiguration" })
);
const toggleLabel = erstelleElement("label", { klasse: "schalter" });
const toggleInput = erstelleElement("input", {
type: "checkbox",
checked: Boolean(tool.enabled),
onchange: async (e) => {
const aktiv = e.target.checked;
try {
await api(
`/agents/${encodeURIComponent(agent.agentId)}/tools/${encodeURIComponent(tool.toolName)}?enabled=${aktiv}`,
{ method: "PUT" }
);
tool.enabled = aktiv;
zeigeMeldung(`Werkzeug '${tool.toolName}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
} catch (err) {
e.target.checked = !aktiv;
zeigeMeldung(`Änderung fehlgeschlagen: ${err.message}`, "gefahr");
}
}
});
const toggleSpan = erstelleElement("span", { klasse: "schalter-regler" });
toggleLabel.append(toggleInput, toggleSpan);
rechts.append(einstellungenBtn, toggleLabel);
zeile.append(links, rechts);
werkzeugeListe.append(zeile);
}
}
const werkzeugeKarte = erstelleKarte("Verfügbare Werkzeuge", [werkzeugeListe]);
detailContainer.append(basisKarte, werkzeugeKarte);
}
function oeffneNeuerAgentDialog() {
const formular = erstelleElement("form", { klasse: "formular-raster" });
const idFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: "z.B. scout",
id: "neu-agent-id"
});
const nameFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: "z.B. Scout Agent",
id: "neu-agent-name"
});
const modellFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: "z.B. anthropic/claude-3.5-sonnet",
value: "anthropic/claude-3.5-sonnet",
id: "neu-agent-modell"
});
const descFeld = erstelleElement("textarea", {
rows: "3",
placeholder: "Optionale Beschreibung des Agenten…",
id: "neu-agent-desc"
});
formular.append(
erstelleElement("label", { text: "Agent-ID (eindeutiger Schlüssel):" }),
idFeld,
erstelleElement("label", { text: "Anzeigename:" }),
nameFeld,
erstelleElement("label", { text: "Modell (OpenRouter):" }),
modellFeld,
erstelleElement("label", { text: "Beschreibung:" }),
descFeld
);
const abbrechenBtn = erstelleElement(
"button",
{
type: "button",
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen()
},
erstelleElement("span", { text: "Abbrechen" })
);
const speichernBtn = erstelleElement(
"button",
{
type: "submit",
klasse: "schaltflaeche primaer"
},
erstelleElement("span", { text: "Agent anlegen" })
);
const modal = erstelleModal("Neuen Agenten anlegen", formular, [abbrechenBtn, speichernBtn]);
formular.addEventListener("submit", async (e) => {
e.preventDefault();
speichernBtn.disabled = true;
try {
await api("/agents", {
method: "POST",
body: {
agentId: idFeld.value.trim(),
displayName: nameFeld.value.trim(),
model: modellFeld.value.trim(),
description: descFeld.value.trim()
}
});
modal.schliessen();
zeigeMeldung(`Agent '${nameFeld.value.trim()}' wurde erfolgreich angelegt.`, "erfolg");
await ladeUndRendereListe(idFeld.value.trim());
} catch (err) {
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
} finally {
speichernBtn.disabled = false;
}
});
modal.oeffnen();
}
async function loescheAgent(agentId, name) {
if (!confirm(`Soll der Agent '${name}' (${agentId}) wirklich gelöscht werden?`)) return;
try {
await api(`/agents/${encodeURIComponent(agentId)}`, { method: "DELETE" });
zeigeMeldung(`Agent '${name}' wurde gelöscht.`, "info");
await ladeUndRendereListe();
} catch (err) {
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
}
}
async function zeigeWerkzeugEinstellungen(agentId, toolName) {
const box = erstelleElement("div", { klasse: "einstellungen-box" });
box.append(erstelleElement("p", { klasse: "gedaempft", text: "Lade Konfiguration…" }));
const schliessenBtn = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen(),
text: "Schließen"
});
const modal = erstelleModal(
`Einstellungen: ${toolName} (${agentId})`,
box,
[schliessenBtn]
);
modal.oeffnen();
try {
const daten = await api(
`/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolName)}/settings`
);
box.replaceChildren();
const hinweis = erstelleElement("p", {
klasse: "hinweis",
text: "Geheime Werte (Passwörter, Schlüssel) werden vom Server maskiert ausgegeben."
});
box.append(hinweis);
if (!daten || Object.keys(daten).length === 0) {
box.append(
erstelleElement("p", { klasse: "gedaempft", text: "Keine individuellen Einstellungen hinterlegt." })
);
return;
}
const dl = erstelleElement("dl", { klasse: "datenliste" });
for (const [schluessel, wert] of Object.entries(daten)) {
const dt = erstelleElement("dt", { text: schluessel });
const dd = erstelleElement("dd", { text: typeof wert === "object" ? JSON.stringify(wert) : String(wert) });
dl.append(dt, dd);
}
box.append(dl);
} catch (err) {
box.replaceChildren(
erstelleElement("p", { klasse: "fehler", text: `Laden fehlgeschlagen: ${err.message}` })
);
}
}