feat(diagnostic): Agent-Historie pro Agent — Akkordeon je Nachricht
Jede Host-Aktion wird als Historie-Eintrag festgehalten und in der Diagnostic pro User-Nachricht gruppiert aufklappbar dargestellt (mit Screenshot-Thumbnails). - Brain: trace_id+msg pro chat(); _dispatch_host als Wrapper um _dispatch_host_inner emittiert je Aktion einen Eintrag an POST /internal/agent-history (Datei-Pfad aus Screenshot-Text erkannt). - Bridge: /internal/agent-history persistiert nach /shared/config/agent_history/ <hostId>.jsonl (Ringpuffer 200) + broadcastet agent_history; agent_history_query -> agent_history_list. - RVS: agent_history/_query/_list in ALLOWED_TYPES (sonst still verworfen). - Diagnostic-Server: relay browser<->RVS + /uploads/<file> Bild-Endpoint (nur /shared/uploads, kein Traversal). - index.html: 'Historie'-Button je Host, Modal mit Akkordeon (Gruppe=Nachricht, aufklappbar, Screenshot-Thumbs), Live-Refresh bei agent_history. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+106
-1
@@ -1594,6 +1594,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent-Historie-Modal (Befehle + Screenshots pro Nachricht) -->
|
||||
<div class="modal-overlay" id="agent-history-modal">
|
||||
<div class="modal-box" style="max-width:760px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="agent-history-title">Historie</h3>
|
||||
<button class="modal-close" onclick="closeAgentHistory()">×</button>
|
||||
</div>
|
||||
<div style="padding:6px 16px;color:#8888AA;font-size:11px;">
|
||||
Jede Zeile = eine Anfrage an ARIA. Aufklappen zeigt, was auf diesem Agenten dafür gemacht wurde.
|
||||
</div>
|
||||
<div class="modal-body" id="agent-history-body" style="padding:10px 16px 16px;max-height:70vh;overflow-y:auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory-Modal (Neu + Editieren) -->
|
||||
<div class="modal-overlay" id="memory-modal">
|
||||
<div class="modal-box" style="max-width:640px;">
|
||||
@@ -2185,6 +2199,19 @@
|
||||
}
|
||||
if (msg.type === 'host_update') { hosts = msg.hosts || []; renderHosts(); return; }
|
||||
if (msg.type === 'rooms_info') { rooms = (msg.payload && msg.payload.rooms) || []; renderRooms(); return; }
|
||||
if (msg.type === 'agent_history_list') {
|
||||
if (agentHistoryHost && msg.payload && msg.payload.host === agentHistoryHost) {
|
||||
renderAgentHistory(msg.payload.events || []);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'agent_history') {
|
||||
// Live: wenn das Modal fuer diesen Host offen ist, neu abfragen (einfach + robust).
|
||||
if (agentHistoryHost && msg.payload && msg.payload.host === agentHistoryHost) {
|
||||
send({ action: 'agent_history_query', host: agentHistoryHost });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'sat_creds_list_result') {
|
||||
const p = msg.payload || msg;
|
||||
const sat = p.satellite || '';
|
||||
@@ -4643,13 +4670,91 @@
|
||||
const caps = (h.caps || []).join(', ') || '—';
|
||||
const ctrl = h.control ? '<span style="color:#0096FF;">steuerbar</span>' : '<span style="color:#FF6E6E;">CONTROL_ENABLED=false</span>';
|
||||
return '<div style="border:1px solid #1E1E2E;border-radius:8px;padding:10px;margin-bottom:8px;">' +
|
||||
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dot + ';margin-right:6px;"></span>' +
|
||||
'<div style="display:flex;align-items:center;gap:8px;">' +
|
||||
'<div style="flex:1;"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dot + ';margin-right:6px;"></span>' +
|
||||
'<strong style="color:#E0E0F0;">💻 ' + escapeHtml(h.name || h.hostId) + '</strong> ' +
|
||||
'<span style="color:#666;font-size:11px;">id=' + escapeHtml(h.hostId) + '</span></div>' +
|
||||
'<button onclick="openAgentHistory(\'' + escapeHtml(h.hostId) + '\')" style="background:#12121E;color:#FFD60A;border:1px solid #FFD60A44;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;">🕘 Historie</button>' +
|
||||
'</div>' +
|
||||
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">' + escapeHtml(h.os || '') + ' · kann: ' + escapeHtml(caps) + ' · ' + ctrl + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
// ── Agent-Historie (Befehle + Screenshots pro Nachricht) ──────────
|
||||
let agentHistoryHost = null;
|
||||
|
||||
function openAgentHistory(hostId) {
|
||||
const h = (hosts || []).find(x => x.hostId === hostId);
|
||||
agentHistoryHost = hostId;
|
||||
document.getElementById('agent-history-title').textContent =
|
||||
'Historie: ' + ((h && (h.name || h.hostId)) || hostId);
|
||||
document.getElementById('agent-history-body').innerHTML =
|
||||
'<span style="color:#8888AA;">lade …</span>';
|
||||
document.getElementById('agent-history-modal').classList.add('open');
|
||||
send({ action: 'agent_history_query', host: hostId });
|
||||
}
|
||||
function closeAgentHistory() {
|
||||
document.getElementById('agent-history-modal').classList.remove('open');
|
||||
agentHistoryHost = null;
|
||||
}
|
||||
function toggleHistGroup(gi) {
|
||||
const el = document.getElementById('hist-group-' + gi);
|
||||
const caret = document.getElementById('hist-caret-' + gi);
|
||||
if (!el) return;
|
||||
const open = el.style.display !== 'none';
|
||||
el.style.display = open ? 'none' : 'block';
|
||||
if (caret) caret.textContent = open ? '▶' : '▼';
|
||||
}
|
||||
function renderAgentHistory(events) {
|
||||
const body = document.getElementById('agent-history-body');
|
||||
if (!body) return;
|
||||
if (!events || !events.length) {
|
||||
body.innerHTML = '<span style="color:#8888AA;">Noch keine Aktionen für diesen Agenten aufgezeichnet. ' +
|
||||
'Sobald ARIA etwas auf ihm tut (Screenshot, tippen, …), erscheint es hier.</span>';
|
||||
return;
|
||||
}
|
||||
// Nach trace_id gruppieren (eine Gruppe = eine User-Nachricht), Reihenfolge erhalten.
|
||||
const groups = [], idx = {};
|
||||
events.forEach(ev => {
|
||||
const key = ev.trace_id || ('_' + ev.ts);
|
||||
if (!(key in idx)) { idx[key] = groups.length; groups.push({ msg: ev.msg || '', ts: ev.ts, items: [] }); }
|
||||
groups[idx[key]].items.push(ev);
|
||||
});
|
||||
groups.reverse(); // neueste Nachricht oben
|
||||
body.innerHTML = groups.map((g, gi) => {
|
||||
const t = new Date(g.ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
const bad = g.items.some(e => e.ok === false);
|
||||
const dot = bad ? '#FF9500' : '#34C759';
|
||||
const head = '<div onclick="toggleHistGroup(' + gi + ')" style="cursor:pointer;padding:8px 10px;background:#12121E;border-radius:6px;display:flex;align-items:center;gap:8px;">' +
|
||||
'<span id="hist-caret-' + gi + '" style="color:#8888AA;">' + (gi === 0 ? '▼' : '▶') + '</span>' +
|
||||
'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dot + ';"></span>' +
|
||||
'<span style="color:#666;font-size:11px;">' + t + '</span>' +
|
||||
'<span style="color:#E0E0F0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(g.msg || '(ohne Text)') + '</span>' +
|
||||
'<span style="color:#8888AA;font-size:11px;white-space:nowrap;">' + g.items.length + ' Aktion(en)</span></div>';
|
||||
const rows = g.items.map(e => {
|
||||
const good = e.ok !== false;
|
||||
const ic = good ? '✓' : '✗';
|
||||
const icColor = good ? '#34C759' : '#FF6E6E';
|
||||
const args = (e.args && Object.keys(e.args).length)
|
||||
? ' <span style="color:#666;font-size:11px;">' + escapeHtml(JSON.stringify(e.args)) + '</span>' : '';
|
||||
let thumb = '';
|
||||
if (e.file) {
|
||||
const base = String(e.file).split('/').pop();
|
||||
thumb = '<div style="margin-top:5px;"><img src="/uploads/' + encodeURIComponent(base) +
|
||||
'" loading="lazy" style="max-width:180px;max-height:320px;border:1px solid #1E1E2E;border-radius:6px;cursor:pointer;" ' +
|
||||
'onclick="window.open(this.src)" onerror="this.style.display=\'none\'"></div>';
|
||||
}
|
||||
return '<div style="padding:6px 10px 6px 26px;border-bottom:1px solid #15151F;">' +
|
||||
'<span style="color:' + icColor + ';">' + ic + '</span> ' +
|
||||
'<code style="color:#FFD60A;">' + escapeHtml(e.action || e.tool || '?') + '</code>' + args +
|
||||
(e.summary ? '<div style="color:#8888AA;font-size:11px;margin-top:2px;">' + escapeHtml(e.summary) + '</div>' : '') +
|
||||
thumb + '</div>';
|
||||
}).join('');
|
||||
return '<div style="margin-bottom:6px;">' + head +
|
||||
'<div id="hist-group-' + gi + '" style="display:' + (gi === 0 ? 'block' : 'none') + ';">' + rows + '</div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function scanSatellite(id) {
|
||||
satScanning = id;
|
||||
renderSatellites();
|
||||
|
||||
@@ -1098,6 +1098,10 @@ function connectRVS(forcePlain) {
|
||||
} else if (msg.type === "rooms_info") {
|
||||
// Antwort des RVS auf rooms_query → an den Browser (Raum-Diagnose).
|
||||
broadcast({ type: "rooms_info", payload: msg.payload || {} });
|
||||
} else if (msg.type === "agent_history" || msg.type === "agent_history_list") {
|
||||
// Live-Event (agent_history) bzw. Abfrage-Antwort (agent_history_list)
|
||||
// der Bridge → an den Browser (Agent-Historie).
|
||||
broadcast({ type: msg.type, payload: msg.payload || {} });
|
||||
} else if (msg.type === "sat_devices") {
|
||||
// Antwort eines Satelliten auf sat_discover → Geraeteliste an Browser.
|
||||
const p = msg.payload || {};
|
||||
@@ -1944,6 +1948,19 @@ const server = http.createServer((req, res) => {
|
||||
"Expires": "0",
|
||||
});
|
||||
res.end(fs.readFileSync(htmlPath, "utf-8"));
|
||||
} else if (req.url.startsWith("/uploads/")) {
|
||||
// Screenshots aus der Agent-Historie (nur /shared/uploads, kein Traversal).
|
||||
const base = path.basename(decodeURIComponent(req.url.slice("/uploads/".length)));
|
||||
const file = path.join("/shared/uploads", base);
|
||||
if (!file.startsWith("/shared/uploads/") || !fs.existsSync(file)) {
|
||||
res.writeHead(404); res.end("not found"); return;
|
||||
}
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
const ct = ext === ".png" ? "image/png"
|
||||
: (ext === ".jpg" || ext === ".jpeg") ? "image/jpeg"
|
||||
: ext === ".webp" ? "image/webp" : "application/octet-stream";
|
||||
res.writeHead(200, { "Content-Type": ct, "Cache-Control": "max-age=86400" });
|
||||
res.end(fs.readFileSync(file));
|
||||
} else if (req.url === "/api/state") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ state, logs: logs.slice(-100) }));
|
||||
@@ -2796,6 +2813,11 @@ wss.on("connection", (ws) => {
|
||||
// Browser will die RVS-Raum-Diagnose — via persistente rvsWs anfragen,
|
||||
// die Antwort (rooms_info) kommt auf derselben Verbindung zurueck.
|
||||
sendToRVS_raw({ type: "rooms_query", payload: {}, timestamp: Date.now() });
|
||||
} else if (msg.action === "agent_history_query") {
|
||||
// Browser will die gespeicherte Historie eines Agenten — die Bridge
|
||||
// antwortet mit agent_history_list (auf derselben rvsWs).
|
||||
sendToRVS_raw({ type: "agent_history_query",
|
||||
payload: { host: String(msg.host || "") }, timestamp: Date.now() });
|
||||
} else if (msg.action === "worker_list") {
|
||||
// Browser will die aktuelle Compute-Flotte.
|
||||
ws.send(JSON.stringify({ type: "worker_update", workers: workerList() }));
|
||||
|
||||
Reference in New Issue
Block a user