Befunde aus dem Testbetrieb auf der Linux-VM behoben (#572)
- #578: Abgebrochene Laeufe (Schrittgrenze, Zeitueberschreitung, Abbruch, Ausnahme) tragen ihre bezahlten Token - vorher 0 in RunUsage, fuer das Budget unsichtbar. - #580: WebFetch prueft die aufgeloeste Zieladresse beim Verbinden; ein erlaubter Name, der auf 127.0.0.1 aufloest, wird gesperrt. - #575: Dateien entstehen unter Unix mit 0600 (AtomicFile, Protokolle, FileRW-Anhaengen, state.db), Protokollverzeichnisse mit 0700. - #576: SIGTERM/SIGINT beenden auch mit Fenster geordnet. - #593: OpenRouter-Statusdienst wird gestartet. - #589: Auftragsnachricht nennt aktuelle Zeit, Zeitzone und Termin. - #581: Unvollstaendiges Argument-JSON erreicht das Werkzeug nicht mehr; das Modell bekommt eine verstaendliche Rueckmeldung statt Parsertext. - #591: Weboberflaeche liest die Felder, die API und Ereignisstrom liefern (Freigaben, Protokoll, Taskboard mit Termin, Werkzeug-Einstellungen, Tokennutzung); Ereignisstrom in camelCase. - #592: Chat zeigt Nachrichten nicht mehr doppelt. - #536 (teilweise): A2A-Antworten tragen Absender und Empfaenger, Parser wirft nicht mehr; Datums- und Volltextfilter in Agenten-Chats. - deploy/publish.py: Packager-Pfad net10.0, Schalter --agent-file. - Version 0.1.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
// ClawdDotNet — Interne Agenten-Kommunikation A2A (Vorhaben #208)
|
||||
//
|
||||
// Analyse und Protokollierung des Nachrichtenaustauschs zwischen Agenten.
|
||||
// - Filterbar nach beteiligten Agenten
|
||||
// - Dialog-Timeline zwischen Quelle und Ziel
|
||||
// - Filterbar nach beteiligtem Agenten, Zeitraum und Volltext
|
||||
// - Jede Zeile trägt Absender ➔ Empfänger — auch Antworten (#536)
|
||||
// - Live-Aktualisierung über Server-Sent Events (SSE "chat" mit source === "agentcomm")
|
||||
// Strikt DOM API, kein innerHTML.
|
||||
|
||||
@@ -19,6 +19,30 @@ import {
|
||||
|
||||
let sseAbmeldung = null;
|
||||
|
||||
const PRAEFIX = "[Nachricht von Agent \"";
|
||||
|
||||
/**
|
||||
* Liest aus "[Nachricht von Agent "Name" (id)]\n\nText" Kennung, Name und Text — dieselbe
|
||||
* Regel wie AgentCommunicationBuilder.LeseAbsender auf dem Server (nur für Live-Ereignisse).
|
||||
*/
|
||||
function leseAbsender(content) {
|
||||
if (!content.startsWith(PRAEFIX)) return { id: null, name: null, inhalt: content };
|
||||
|
||||
let name = null;
|
||||
let id = null;
|
||||
const endQuote = content.indexOf('"', PRAEFIX.length);
|
||||
if (endQuote > PRAEFIX.length) {
|
||||
name = content.slice(PRAEFIX.length, endQuote);
|
||||
const openParen = content.indexOf("(", endQuote);
|
||||
const closeParen = openParen < 0 ? -1 : content.indexOf(")", openParen);
|
||||
if (openParen > 0 && closeParen > openParen) {
|
||||
id = content.slice(openParen + 1, closeParen);
|
||||
}
|
||||
}
|
||||
const doubleNl = content.indexOf("\n\n");
|
||||
return { id, name, inhalt: doubleNl > 0 ? content.slice(doubleNl + 2) : content };
|
||||
}
|
||||
|
||||
export async function rendereAgentenChats() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
@@ -44,10 +68,26 @@ export async function rendereAgentenChats() {
|
||||
const agentSelect = erstelleElement("select", {
|
||||
klasse: "eingabe",
|
||||
stil: { minWidth: "180px" },
|
||||
title: "Nach Agent filtern"
|
||||
title: "Nach beteiligtem Agenten filtern (Absender oder Empfänger)"
|
||||
});
|
||||
agentSelect.append(erstelleElement("option", { value: "", text: "Alle Agenten" }));
|
||||
|
||||
const vonEingabe = erstelleElement("input", {
|
||||
klasse: "eingabe",
|
||||
type: "date",
|
||||
title: "Nachrichten ab diesem Tag"
|
||||
});
|
||||
const bisEingabe = erstelleElement("input", {
|
||||
klasse: "eingabe",
|
||||
type: "date",
|
||||
title: "Nachrichten bis einschließlich diesem Tag"
|
||||
});
|
||||
const suchEingabe = erstelleElement("input", {
|
||||
klasse: "eingabe",
|
||||
type: "search",
|
||||
placeholder: "Volltext…",
|
||||
title: "Suche im Nachrichtentext und in den Agentennamen"
|
||||
});
|
||||
const optAlle = erstelleElement("option", { value: "", text: "Alle Agenten" });
|
||||
agentSelect.append(optAlle);
|
||||
|
||||
const aktualisierenBtn = erstelleElement(
|
||||
"button",
|
||||
@@ -60,7 +100,15 @@ export async function rendereAgentenChats() {
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
);
|
||||
|
||||
filterGruppe.append(agentSelect, aktualisierenBtn);
|
||||
filterGruppe.append(
|
||||
agentSelect,
|
||||
erstelleElement("span", { klasse: "gedaempft", text: "von" }),
|
||||
vonEingabe,
|
||||
erstelleElement("span", { klasse: "gedaempft", text: "bis" }),
|
||||
bisEingabe,
|
||||
suchEingabe,
|
||||
aktualisierenBtn
|
||||
);
|
||||
kopf.append(titelGruppe, filterGruppe);
|
||||
layout.append(kopf);
|
||||
|
||||
@@ -81,37 +129,41 @@ export async function rendereAgentenChats() {
|
||||
const infoKarte = erstelleKarte("Interne Kommunikation", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Hier wird der direkte Nachrichtenaustausch zwischen Agenten (A2A) protokolliert, wenn Agenten über das Werkzeug 'SendMessage' miteinander interagieren."
|
||||
text: "Hier wird der direkte Nachrichtenaustausch zwischen Agenten protokolliert, wenn Agenten über das Werkzeug AgentComm (Aktion send_message) miteinander sprechen."
|
||||
})
|
||||
]);
|
||||
layout.append(infoKarte);
|
||||
|
||||
// Verlauf-Container
|
||||
const trefferZeile = erstelleElement("p", { klasse: "gedaempft text-klein", text: "" });
|
||||
const verlaufContainer = erstelleElement("div", { klasse: "a2a-timeline" });
|
||||
layout.append(verlaufContainer);
|
||||
layout.append(trefferZeile, verlaufContainer);
|
||||
container.append(layout);
|
||||
|
||||
let alleNachrichten = [];
|
||||
let agentenListe = [];
|
||||
|
||||
// Wer hat einen Agenten zuletzt angeschrieben? Für die Empfänger live eintreffender Antworten.
|
||||
const letzterFragender = new Map();
|
||||
|
||||
// Agenten laden für Filter
|
||||
try {
|
||||
const agenten = await api("/agents");
|
||||
agentenListe = Array.isArray(agenten) ? agenten : [];
|
||||
for (const a of agentenListe) {
|
||||
const opt = erstelleElement("option", {
|
||||
agentSelect.append(erstelleElement("option", {
|
||||
value: a.agentId,
|
||||
text: a.displayName || a.agentId
|
||||
});
|
||||
agentSelect.append(opt);
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// Wenn Agenten nicht ladbar sind, Filter leer lassen
|
||||
}
|
||||
|
||||
agentSelect.addEventListener("change", () => {
|
||||
rendereNachrichten();
|
||||
});
|
||||
for (const eingabe of [agentSelect, vonEingabe, bisEingabe]) {
|
||||
eingabe.addEventListener("change", () => rendereNachrichten());
|
||||
}
|
||||
suchEingabe.addEventListener("input", () => rendereNachrichten());
|
||||
|
||||
async function ladeVerlauf() {
|
||||
verlaufContainer.replaceChildren(
|
||||
@@ -121,6 +173,11 @@ export async function rendereAgentenChats() {
|
||||
try {
|
||||
const daten = await api("/agents/communications");
|
||||
alleNachrichten = Array.isArray(daten) ? daten : [];
|
||||
for (const m of alleNachrichten) {
|
||||
if (m.role === "user" && m.sourceAgentId) {
|
||||
letzterFragender.set(m.targetAgentId, { id: m.sourceAgentId, name: m.sourceAgentName });
|
||||
}
|
||||
}
|
||||
rendereNachrichten();
|
||||
} catch (err) {
|
||||
verlaufContainer.replaceChildren(
|
||||
@@ -134,23 +191,41 @@ export async function rendereAgentenChats() {
|
||||
}
|
||||
}
|
||||
|
||||
function passtZumFilter(m) {
|
||||
const agent = agentSelect.value;
|
||||
if (agent && m.targetAgentId !== agent && m.sourceAgentId !== agent) return false;
|
||||
|
||||
// Die Datumsfelder liefern "JJJJ-MM-TT" im Ortskalender; verglichen wird der lokale Tag.
|
||||
const zeit = new Date(m.timestamp);
|
||||
if (vonEingabe.value && zeit < new Date(`${vonEingabe.value}T00:00:00`)) return false;
|
||||
if (bisEingabe.value && zeit > new Date(`${bisEingabe.value}T23:59:59.999`)) return false;
|
||||
|
||||
const suche = suchEingabe.value.trim().toLowerCase();
|
||||
if (suche) {
|
||||
const heuhaufen = [
|
||||
m.content, m.sourceAgentName, m.sourceAgentId, m.targetAgentName, m.targetAgentId
|
||||
].filter(Boolean).join(" ").toLowerCase();
|
||||
if (!heuhaufen.includes(suche)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function rendereNachrichten() {
|
||||
const gewaehlterAgent = agentSelect.value;
|
||||
const gefiltert = gewaehlterAgent
|
||||
? alleNachrichten.filter(
|
||||
(m) =>
|
||||
m.targetAgentId === gewaehlterAgent ||
|
||||
m.sourceAgentId === gewaehlterAgent
|
||||
)
|
||||
: alleNachrichten;
|
||||
const gefiltert = alleNachrichten.filter(passtZumFilter);
|
||||
const filterAktiv = agentSelect.value || vonEingabe.value || bisEingabe.value || suchEingabe.value.trim();
|
||||
|
||||
trefferZeile.textContent = filterAktiv
|
||||
? `${gefiltert.length} von ${alleNachrichten.length} Nachrichten`
|
||||
: `${alleNachrichten.length} Nachrichten`;
|
||||
|
||||
if (gefiltert.length === 0) {
|
||||
verlaufContainer.replaceChildren(
|
||||
erstelleKarte("Keine Nachrichten", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: gewaehlterAgent
|
||||
? "Für den ausgewählten Agenten liegt noch keine interne Kommunikation vor."
|
||||
text: filterAktiv
|
||||
? "Für diese Filter liegt keine interne Kommunikation vor."
|
||||
: "In dieser Instanz wurde bisher keine interne Agent-zu-Agent-Kommunikation protokolliert."
|
||||
})
|
||||
])
|
||||
@@ -158,12 +233,7 @@ export async function rendereAgentenChats() {
|
||||
return;
|
||||
}
|
||||
|
||||
verlaufContainer.replaceChildren();
|
||||
|
||||
for (const m of gefiltert) {
|
||||
const eintrag = erstelleNachrichtElement(m);
|
||||
verlaufContainer.append(eintrag);
|
||||
}
|
||||
verlaufContainer.replaceChildren(...gefiltert.map(erstelleNachrichtElement));
|
||||
}
|
||||
|
||||
function erstelleNachrichtElement(m) {
|
||||
@@ -175,22 +245,14 @@ export async function rendereAgentenChats() {
|
||||
const kopf = erstelleElement("div", { klasse: "a2a-eintrag-kopf" });
|
||||
const absenderGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
if (istAntwort) {
|
||||
// Antwort vom Zielagenten
|
||||
absenderGruppe.append(
|
||||
erstellePlakette(m.targetAgentName || m.targetAgentId, "erfolg"),
|
||||
erstelleElement("span", { klasse: "a2a-pfeil", text: "➔ Antwort" })
|
||||
);
|
||||
} else {
|
||||
// Anfrage/Nachricht von Quelle an Ziel
|
||||
const quelleName = m.sourceAgentName || m.sourceAgentId || "Agent";
|
||||
const zielName = m.targetAgentName || m.targetAgentId;
|
||||
absenderGruppe.append(
|
||||
erstellePlakette(quelleName, "neutral"),
|
||||
erstelleElement("span", { klasse: "a2a-pfeil", text: "➔" }),
|
||||
erstellePlakette(zielName, "primaer")
|
||||
);
|
||||
}
|
||||
const quelleName = m.sourceAgentName || m.sourceAgentId || "unbekannt";
|
||||
const zielName = m.targetAgentName || m.targetAgentId || "unbekannt";
|
||||
|
||||
absenderGruppe.append(
|
||||
erstellePlakette(quelleName, istAntwort ? "erfolg" : "neutral"),
|
||||
erstelleElement("span", { klasse: "a2a-pfeil", text: istAntwort ? "➔ Antwort an" : "➔" }),
|
||||
erstellePlakette(zielName, istAntwort ? "neutral" : "primaer")
|
||||
);
|
||||
|
||||
const zeit = erstelleElement("span", {
|
||||
klasse: "a2a-zeit",
|
||||
@@ -210,57 +272,41 @@ export async function rendereAgentenChats() {
|
||||
|
||||
// SSE Live-Aktualisierung
|
||||
sseAbmeldung = abonnieren("chat", (daten) => {
|
||||
if (daten?.source === "agentcomm") {
|
||||
// Neue Nachricht kam live herein: an die Liste anhängen
|
||||
const zielAgent = agentenListe.find((a) => a.agentId === daten.agentId);
|
||||
const zielName = zielAgent ? zielAgent.displayName : daten.agentId;
|
||||
if (daten?.source !== "agentcomm") return;
|
||||
|
||||
let senderId = null;
|
||||
let senderName = null;
|
||||
let content = daten.content || "";
|
||||
const agent = agentenListe.find((a) => a.agentId === daten.agentId);
|
||||
const agentName = agent ? agent.displayName : daten.agentId;
|
||||
let neueNachricht;
|
||||
|
||||
if (daten.role === "user" && content.startsWith("[Nachricht von Agent \"")) {
|
||||
const endQuote = content.indexOf('"', 22);
|
||||
if (endQuote > 22) {
|
||||
senderName = content.slice(22, endQuote);
|
||||
const openParen = content.indexOf("(", endQuote);
|
||||
const closeParen = content.indexOf(")", openParen);
|
||||
if (openParen > 0 && closeParen > openParen) {
|
||||
senderId = content.slice(openParen + 1, closeParen);
|
||||
}
|
||||
}
|
||||
const doubleNl = content.indexOf("\n\n");
|
||||
if (doubleNl > 0) {
|
||||
content = content.slice(doubleNl + 2);
|
||||
}
|
||||
}
|
||||
|
||||
const neueNachricht = {
|
||||
if (daten.role === "user") {
|
||||
const { id, name, inhalt } = leseAbsender(daten.content || "");
|
||||
if (id) letzterFragender.set(daten.agentId, { id, name });
|
||||
neueNachricht = {
|
||||
targetAgentId: daten.agentId,
|
||||
targetAgentName: zielName,
|
||||
sourceAgentId: senderId,
|
||||
sourceAgentName: senderName,
|
||||
targetAgentName: agentName,
|
||||
sourceAgentId: id,
|
||||
sourceAgentName: name,
|
||||
role: daten.role,
|
||||
content: content,
|
||||
content: inhalt,
|
||||
timestamp: daten.timestamp || new Date().toISOString()
|
||||
};
|
||||
|
||||
alleNachrichten.push(neueNachricht);
|
||||
|
||||
const gewaehlterAgent = agentSelect.value;
|
||||
if (
|
||||
!gewaehlterAgent ||
|
||||
neueNachricht.targetAgentId === gewaehlterAgent ||
|
||||
neueNachricht.sourceAgentId === gewaehlterAgent
|
||||
) {
|
||||
// Falls vorher "Keine Nachrichten" da stand:
|
||||
const leerKarte = verlaufContainer.querySelector(".karte");
|
||||
if (leerKarte) verlaufContainer.replaceChildren();
|
||||
|
||||
const el = erstelleNachrichtElement(neueNachricht);
|
||||
verlaufContainer.append(el);
|
||||
}
|
||||
} else if (daten.role === "assistant") {
|
||||
const fragender = letzterFragender.get(daten.agentId);
|
||||
neueNachricht = {
|
||||
targetAgentId: fragender?.id || "",
|
||||
targetAgentName: fragender?.name || fragender?.id || "",
|
||||
sourceAgentId: daten.agentId,
|
||||
sourceAgentName: agentName,
|
||||
role: daten.role,
|
||||
content: daten.content || "",
|
||||
timestamp: daten.timestamp || new Date().toISOString()
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
alleNachrichten.push(neueNachricht);
|
||||
rendereNachrichten();
|
||||
});
|
||||
|
||||
ladeVerlauf();
|
||||
|
||||
@@ -215,7 +215,7 @@ async function rendereDetails(agentId) {
|
||||
const zeile = erstelleElement("div", { klasse: "werkzeug-zeile" });
|
||||
|
||||
const links = erstelleElement("div", { klasse: "werkzeug-info" });
|
||||
const toolTitel = erstelleElement("strong", { text: tool.toolName });
|
||||
const toolTitel = erstelleElement("strong", { text: tool.name });
|
||||
const toolDesc = erstelleElement("div", {
|
||||
klasse: "werkzeug-beschreibung gedaempft",
|
||||
text: tool.description || ""
|
||||
@@ -229,7 +229,7 @@ async function rendereDetails(agentId) {
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
title: "Werkzeug-Einstellungen einsehen",
|
||||
onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.toolName)
|
||||
onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.name)
|
||||
},
|
||||
erstelleIcon(PFADE.einstellungen, 14),
|
||||
erstelleElement("span", { text: " Konfiguration" })
|
||||
@@ -243,11 +243,11 @@ async function rendereDetails(agentId) {
|
||||
const aktiv = e.target.checked;
|
||||
try {
|
||||
await api(
|
||||
`/agents/${encodeURIComponent(agent.agentId)}/tools/${encodeURIComponent(tool.toolName)}?enabled=${aktiv}`,
|
||||
`/agents/${encodeURIComponent(agent.agentId)}/tools/${encodeURIComponent(tool.name)}?enabled=${aktiv}`,
|
||||
{ method: "PUT" }
|
||||
);
|
||||
tool.enabled = aktiv;
|
||||
zeigeMeldung(`Werkzeug '${tool.toolName}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
|
||||
zeigeMeldung(`Werkzeug '${tool.name}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
|
||||
} catch (err) {
|
||||
e.target.checked = !aktiv;
|
||||
zeigeMeldung(`Änderung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
|
||||
@@ -24,14 +24,25 @@ let agentenSelector = null;
|
||||
let statusPlakette = null;
|
||||
let entladeEreignisse = [];
|
||||
|
||||
// #592: Zählt die Aufbauten dieser Ansicht. Ein Aufbau, der während seines await von
|
||||
// einem neueren überholt wurde, meldet keine Ereignisse mehr an — sonst hingen zwei
|
||||
// "chat"-Listener und jede Nachricht erschien doppelt.
|
||||
let aufbauZaehler = 0;
|
||||
|
||||
function raeumeEreignisseAuf() {
|
||||
entladeEreignisse.forEach((cleanup) => cleanup());
|
||||
entladeEreignisse = [];
|
||||
}
|
||||
|
||||
export async function rendereChat(parameter = {}) {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const dieserAufbau = ++aufbauZaehler;
|
||||
|
||||
// Alte Ereignisse aufräumen
|
||||
entladeEreignisse.forEach((cleanup) => cleanup());
|
||||
entladeEreignisse = [];
|
||||
raeumeEreignisseAuf();
|
||||
|
||||
const chatShell = erstelleElement("div", { klasse: "chat-ansicht" });
|
||||
|
||||
@@ -128,6 +139,12 @@ export async function rendereChat(parameter = {}) {
|
||||
// Lade Agenten und initialisiere
|
||||
await ladeAgenten(parameter.agentId);
|
||||
|
||||
// Während des Ladens neu aufgebaut? Dann gehört die Ansicht dem neueren Aufbau.
|
||||
if (dieserAufbau !== aufbauZaehler) return;
|
||||
|
||||
// Falls zwischendurch doch etwas angemeldet wurde: vorher abräumen, nie doppelt.
|
||||
raeumeEreignisseAuf();
|
||||
|
||||
// SSE-Abonnements
|
||||
entladeEreignisse.push(
|
||||
abonnieren("chat", (daten) => {
|
||||
@@ -203,7 +220,12 @@ async function waehleAgent(agentId) {
|
||||
const agent = agentenListe.find((a) => a.agentId === agentId);
|
||||
aktualisiereLaufStatus(agent?.isRunning ?? false);
|
||||
|
||||
window.location.hash = `#/chat/${encodeURIComponent(agentId)}`;
|
||||
// Adresse mitführen, ohne hashchange auszulösen (#592): Ein Setzen von location.hash
|
||||
// ließ den Router die ganze Ansicht mitten im Laden ein zweites Mal aufbauen.
|
||||
const ziel = `#/chat/${encodeURIComponent(agentId)}`;
|
||||
if (window.location.hash !== ziel) {
|
||||
history.replaceState(null, "", ziel);
|
||||
}
|
||||
|
||||
await ladeChatVerlauf(agentId);
|
||||
}
|
||||
|
||||
@@ -179,9 +179,19 @@ function starteLogStream() {
|
||||
});
|
||||
}
|
||||
|
||||
function fuegeLogZeileEin(eintrag) {
|
||||
function fuegeLogZeileEin(roh) {
|
||||
if (!logContainer) return;
|
||||
|
||||
// Seit #591 kommen die Felder klein geschrieben (module/level/text). Instanzen einer
|
||||
// älteren Fassung senden sie noch groß — beide Schreibweisen annehmen, sonst zeigt
|
||||
// die Ansicht bei gemischten Fassungen wieder nur den Instanznamen.
|
||||
const eintrag = {
|
||||
...roh,
|
||||
module: roh.module ?? roh.Module,
|
||||
level: roh.level ?? roh.Level,
|
||||
text: roh.text ?? roh.Text
|
||||
};
|
||||
|
||||
// Filter nach Instanz falls gewählt
|
||||
if (instanzFilter && eintrag.instanz && eintrag.instanz !== instanzFilter && eintrag.instanzName !== instanzFilter) {
|
||||
return;
|
||||
|
||||
@@ -111,22 +111,24 @@ async function ladeFreigaben() {
|
||||
erstelleElement("dt", { text: "Agent" }),
|
||||
erstelleElement("dd", { text: anfrage.agentId || "—" }),
|
||||
erstelleElement("dt", { text: "Werkzeug" }),
|
||||
erstelleElement("dd", { text: `${anfrage.toolName} → ${anfrage.methodName}` }),
|
||||
erstelleElement("dd", { text: `${anfrage.tool} → ${anfrage.action || "—"}` }),
|
||||
erstelleElement("dt", { text: "Zeitpunkt" }),
|
||||
erstelleElement("dd", { text: formatiereDatumZeit(anfrage.requestedAt) })
|
||||
erstelleElement("dd", { text: formatiereDatumZeit(anfrage.createdAt) })
|
||||
);
|
||||
|
||||
const argBox = erstelleElement("div", { klasse: "staging-argumente" });
|
||||
argBox.append(erstelleElement("strong", { text: "Eingefrorene Argumente:" }));
|
||||
|
||||
// Strikt textContent! Argumente können Fremdtext sein
|
||||
// Strikt textContent! Argumente können Fremdtext sein.
|
||||
// Die API liefert sie als rohe Zeichenkette (argumentsJson) — genau das, was bei
|
||||
// Freigabe ausgeführt würde. Lesbar eingerückt, wenn es gültiges JSON ist; sonst roh.
|
||||
const pre = erstelleElement("pre", {
|
||||
klasse: "code-block",
|
||||
text: JSON.stringify(anfrage.arguments, null, 2)
|
||||
text: formatiereArgumente(anfrage.argumentsJson)
|
||||
});
|
||||
argBox.append(pre);
|
||||
|
||||
const karte = erstelleKarte(`${anfrage.toolName}.${anfrage.methodName}`, [datenliste, argBox], {
|
||||
const karte = erstelleKarte(`${anfrage.tool}.${anfrage.action || "?"}`, [datenliste, argBox], {
|
||||
aktionen: kopfAktionen
|
||||
});
|
||||
|
||||
@@ -144,6 +146,19 @@ async function ladeFreigaben() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die eingefrorenen Argumente lesbar machen, ohne sie zu verändern: eingerückt, wenn es
|
||||
* gültiges JSON ist, sonst genau die Zeichenkette, die ausgeführt würde.
|
||||
*/
|
||||
function formatiereArgumente(argumentsJson) {
|
||||
if (!argumentsJson) return "(keine)";
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(argumentsJson), null, 2);
|
||||
} catch {
|
||||
return argumentsJson;
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisiereStagingBadge(anzahl) {
|
||||
const badge = document.getElementById("staging-badge");
|
||||
if (!badge) return;
|
||||
|
||||
@@ -91,6 +91,10 @@ async function ladeAktivenBereich() {
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
// "Neuer Job" gehoert nur zum Jobs-Reiter; im Taskboard legte er trotzdem einen Job an (#591).
|
||||
const neuerJob = document.querySelector(".aufgaben-ansicht .ansicht-kopf .schaltflaeche.primaer");
|
||||
if (neuerJob) neuerJob.hidden = aktiverReiter !== "jobs";
|
||||
|
||||
if (aktiverReiter === "jobs") {
|
||||
await rendereJobs(container);
|
||||
} else {
|
||||
@@ -369,7 +373,13 @@ async function rendereTaskboard(container) {
|
||||
|
||||
for (const task of tasks) {
|
||||
const karte = erstelleKarte(task.title || `Aufgabe ${task.id}`, [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: `ID: ${task.id} · Agent: ${task.agentId || "—"}` }),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: `ID: ${task.id} · Agent: ${task.assignee || "—"}` }),
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: task.when
|
||||
? `Termin: ${task.when.kind} ${task.when.value} (${task.when.timeZone || "ohne Zeitzone"})`
|
||||
: "Termin: einmalig"
|
||||
}),
|
||||
erstelleElement("div", {
|
||||
klasse: "task-status-zeile",
|
||||
stil: "margin-top: 8px;"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// ClawdDotNet — Token-Verbrauch & Analyse
|
||||
//
|
||||
// Diagramme (CSS/DOM-Balken ohne externe Bibliothek), Statistiken und Läufetabelle.
|
||||
//
|
||||
// Feldnamen wie UsageDto (#591): costUsd, costIsKnown, occurredAt, status. Eine
|
||||
// Gesamtsumme liefert die API nicht — sie ist Prompt + Completion. Vorher las die Ansicht
|
||||
// totalTokens, cost und timestamp und zeigte überall 0 bzw. "—".
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import {
|
||||
@@ -26,6 +30,10 @@ export async function rendereVerbrauch() {
|
||||
await ladeVerbrauch();
|
||||
}
|
||||
|
||||
function gesamtTokens(lauf) {
|
||||
return (lauf.promptTokens || 0) + (lauf.completionTokens || 0);
|
||||
}
|
||||
|
||||
async function ladeVerbrauch() {
|
||||
const container = document.getElementById("verbrauch-inhalt-bereich");
|
||||
if (!container) return;
|
||||
@@ -55,32 +63,45 @@ async function ladeVerbrauch() {
|
||||
let summePrompt = 0;
|
||||
let summeCompletion = 0;
|
||||
let summeKosten = 0;
|
||||
let unbekannteKosten = 0;
|
||||
const nachAgent = new Map();
|
||||
const nachModell = new Map();
|
||||
|
||||
for (const l of laeufe) {
|
||||
summeTokens += l.totalTokens || 0;
|
||||
const tokens = gesamtTokens(l);
|
||||
summeTokens += tokens;
|
||||
summePrompt += l.promptTokens || 0;
|
||||
summeCompletion += l.completionTokens || 0;
|
||||
summeKosten += Number(l.cost || 0);
|
||||
summeKosten += Number(l.costUsd || 0);
|
||||
if (!l.costIsKnown) unbekannteKosten++;
|
||||
|
||||
const agentKey = l.agentId || "Unbekannt";
|
||||
nachAgent.set(agentKey, (nachAgent.get(agentKey) || 0) + (l.totalTokens || 0));
|
||||
nachAgent.set(agentKey, (nachAgent.get(agentKey) || 0) + tokens);
|
||||
|
||||
const modellKey = l.model || "Unbekannt";
|
||||
nachModell.set(modellKey, (nachModell.get(modellKey) || 0) + (l.totalTokens || 0));
|
||||
nachModell.set(modellKey, (nachModell.get(modellKey) || 0) + tokens);
|
||||
}
|
||||
|
||||
// 1. Kacheln (Statistiken)
|
||||
const kachelGitter = erstelleElement("div", { klasse: "kachel-gitter" });
|
||||
|
||||
kachelGitter.append(
|
||||
erstelleStatKachel("Gesamtkosten (100 Läufe)", formatiereKosten(summeKosten)),
|
||||
erstelleStatKachel(`Gesamtkosten (${laeufe.length} Läufe)`, formatiereKosten(summeKosten)),
|
||||
erstelleStatKachel("Tokens gesamt", formatiereTokens(summeTokens)),
|
||||
erstelleStatKachel("Eingabe-Tokens (Prompt)", formatiereTokens(summePrompt)),
|
||||
erstelleStatKachel("Ausgabe-Tokens (Completion)", formatiereTokens(summeCompletion))
|
||||
);
|
||||
|
||||
const hinweise = [];
|
||||
if (unbekannteKosten > 0) {
|
||||
hinweise.push(
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: `${unbekannteKosten} Lauf/Läufe ohne bekannten Preis — deren Kosten sind in der Summe nicht enthalten.`
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Balkendiagramme (rein CSS/DOM)
|
||||
const diagrammGitter = erstelleElement("div", { klasse: "diagramm-gitter" });
|
||||
|
||||
@@ -99,6 +120,7 @@ async function ladeVerbrauch() {
|
||||
erstelleElement("th", { text: "Zeitpunkt" }),
|
||||
erstelleElement("th", { text: "Agent" }),
|
||||
erstelleElement("th", { text: "Modell" }),
|
||||
erstelleElement("th", { text: "Status" }),
|
||||
erstelleElement("th", { text: "Prompt", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Completion", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Gesamt", stil: "text-align: right;" }),
|
||||
@@ -109,15 +131,20 @@ async function ladeVerbrauch() {
|
||||
const tbody = erstelleElement("tbody");
|
||||
|
||||
for (const lauf of laeufe) {
|
||||
const kosten = lauf.costIsKnown
|
||||
? formatiereKosten(lauf.costUsd)
|
||||
: `${formatiereKosten(lauf.costUsd)} (Preis unbekannt)`;
|
||||
|
||||
const tr = erstelleElement("tr");
|
||||
tr.append(
|
||||
erstelleElement("td", { text: formatiereDatumZeit(lauf.timestamp) }),
|
||||
erstelleElement("td", { text: formatiereDatumZeit(lauf.occurredAt) }),
|
||||
erstelleElement("td", {}, erstelleElement("strong", { text: lauf.agentId })),
|
||||
erstelleElement("td", { klasse: "gedaempft text-klein", text: lauf.model }),
|
||||
erstelleElement("td", { klasse: "text-klein", text: lauf.status || "—" }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.promptTokens) }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.completionTokens) }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.totalTokens) }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereKosten(lauf.cost) })
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(gesamtTokens(lauf)) }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: kosten })
|
||||
);
|
||||
tbody.append(tr);
|
||||
}
|
||||
@@ -125,7 +152,7 @@ async function ladeVerbrauch() {
|
||||
tabelle.append(thead, tbody);
|
||||
tabelleKarte.append(tabelle);
|
||||
|
||||
container.append(kachelGitter, diagrammGitter, tabelleKarte);
|
||||
container.append(kachelGitter, ...hinweise, diagrammGitter, tabelleKarte);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Verbrauchsdaten nicht verfügbar", [
|
||||
|
||||
Reference in New Issue
Block a user