feat: Belege — jede ARIA-Antwort zeigt, was sie wirklich ausgefuehrt hat
Was ARIA selbst im Proxy ausfuehrt (Bash/ssh, Read, Edit …), war nur live sichtbar — ob sie fuer "auf Prod nachgesehen" wirklich nachgesehen hat (und wo), liess sich hinterher nicht pruefen. Das Befehlslog gab es nur fuer die Host-Agenten. Proxy (proxy-patches/routes.js): start/tool_use/tool_result/end zusaetzlich dauerhaft nach /shared/logs/aria-tools/<tag>.jsonl (30 Tage), Passwoerter, Tokens, API-Keys, private Schluessel und Zugangsdaten in URLs geschwaerzt. Diagnostic: 🔍 an jeder ARIA-Bubble → alle Werkzeug-Aufrufe zwischen Stefans Frage und dieser Antwort (Befehl + Ausgabe). Hat sie nichts ausgefuehrt, sagt das Fenster es deutlich (Stand kommt dann aus Verlauf/Gedaechtnis). GET /api/aria-proof?ts=&project=. Deploy: proxy --force-recreate (Patch wirkt nur beim Start), diagnostic --build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+66
-2
@@ -74,6 +74,12 @@
|
||||
transition:opacity 0.15s; }
|
||||
.chat-msg:hover .bubble-trash { opacity: 1; }
|
||||
.chat-msg .bubble-trash:hover { background:#FF3B30; color:#fff; }
|
||||
.chat-msg .bubble-proof { position:absolute; top:4px; right:32px; background:rgba(0,150,255,0.15);
|
||||
color:#4DB8FF; border:none; border-radius:50%; width:22px; height:22px;
|
||||
font-size:11px; line-height:18px; padding:0; cursor:pointer; opacity:0;
|
||||
transition:opacity 0.15s; }
|
||||
.chat-msg:hover .bubble-proof { opacity: 1; }
|
||||
.chat-msg .bubble-proof:hover { background:#0096FF; color:#fff; }
|
||||
.chat-msg.sent { background: #0096FF; color: #fff; align-self: flex-end;
|
||||
border-bottom-right-radius: 4px; }
|
||||
.chat-msg.received { background: #1E1E2E; color: #E8E8F0; align-self: flex-start;
|
||||
@@ -197,6 +203,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Belege: Werkzeug-Aufrufe zu einer ARIA-Antwort -->
|
||||
<div id="proof-modal" style="display:none;position:fixed;inset:0;z-index:1003;background:rgba(0,0,0,0.8);align-items:center;justify-content:center;" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:10px;padding:16px 20px;max-width:1000px;width:94%;max-height:92vh;overflow:auto;color:#C8C8D8;font-size:13px;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
||||
<h3 style="margin:0;color:#fff;">🔍 Belege zu dieser Antwort</h3>
|
||||
<button onclick="document.getElementById('proof-modal').style.display='none'" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div id="proof-body">…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bestenlisten: Ranglisten-Links + HF-Trends (direkt in den Katalog) -->
|
||||
<div id="boards-modal" style="display:none;position:fixed;inset:0;z-index:1002;background:rgba(0,0,0,0.8);align-items:center;justify-content:center;" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:10px;padding:16px 20px;max-width:1000px;width:94%;max-height:92vh;overflow:auto;color:#C8C8D8;font-size:13px;">
|
||||
@@ -3005,7 +3022,10 @@
|
||||
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
|
||||
: '';
|
||||
const histBadge = srcBadgeHtml(m.type, m.answeredBy || '', m.model || '', m.modelReason || '');
|
||||
const innerHtml = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)}${histBadge} — ${time}</div>`;
|
||||
const proofBtn = (m.ts && m.type === 'received')
|
||||
? `<button class="bubble-proof" title="Belege: was hat ARIA für diese Antwort wirklich ausgeführt?" onclick="showAriaProof(${m.ts}, this)">🔍</button>`
|
||||
: '';
|
||||
const innerHtml = `${trashBtn}${proofBtn}${linked}<div class="meta">${escapeHtml(m.meta)}${histBadge} — ${time}</div>`;
|
||||
for (const b of boxes) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `chat-msg ${m.type}`;
|
||||
@@ -3661,7 +3681,10 @@
|
||||
// Quell-Badge: welcher Backend die Antwort erzeugt hat (nur ARIA-Bubbles)
|
||||
const srcBadge = srcBadgeHtml(type, (options && options.answeredBy) || '',
|
||||
(options && options.model) || '', (options && options.modelReason) || '');
|
||||
const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${srcBadge} — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
||||
const proofBtn = (backupTs && type === 'received')
|
||||
? `<button class="bubble-proof" title="Belege: was hat ARIA für diese Antwort wirklich ausgeführt?" onclick="showAriaProof(${backupTs}, this)">🔍</button>`
|
||||
: '';
|
||||
const html = `${trashBtn}${proofBtn}${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${srcBadge} — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
||||
|
||||
// Thinking-Indikator ausblenden bei neuer Nachricht
|
||||
updateThinkingIndicator({ activity: 'idle' });
|
||||
@@ -9535,6 +9558,47 @@
|
||||
} catch (e) { btn.disabled = false; btn.textContent = 'In Katalog'; alert(e.message); }
|
||||
}
|
||||
|
||||
// ── Belege: was hat ARIA fuer eine Antwort wirklich ausgefuehrt? ──
|
||||
async function showAriaProof(ts, btn) {
|
||||
const project = (btn && btn.closest('.chat-msg') && btn.closest('.chat-msg').dataset.projectId) || '';
|
||||
const body = document.getElementById('proof-body');
|
||||
document.getElementById('proof-modal').style.display = 'flex';
|
||||
body.innerHTML = '<span style="color:#8888AA;">lade …</span>';
|
||||
let j;
|
||||
try { j = await (await fetch('/api/aria-proof?ts=' + ts + '&project=' + encodeURIComponent(project))).json(); }
|
||||
catch (e) { body.innerHTML = '<span style="color:#FF6B6B;">✗ ' + escapeHtml(e.message) + '</span>'; return; }
|
||||
if (!j.ok) { body.innerHTML = '<span style="color:#FF6B6B;">✗ ' + escapeHtml(j.error || 'Fehler') + '</span>'; return; }
|
||||
const t = x => new Date(x).toLocaleTimeString('de-DE');
|
||||
const head = '<div style="font-size:12px;color:#8888AA;margin-bottom:10px;">Zeitraum ' + t(j.from) + ' – ' + t(j.to) +
|
||||
(project ? ' · Projekt <code>' + escapeHtml(project) + '</code>' : ' · Hauptchat') + '</div>';
|
||||
if (!j.toolCount) {
|
||||
const before = j.logSince && new Date(j.to).toISOString().slice(0, 10) < j.logSince;
|
||||
body.innerHTML = head + '<div style="border:1px solid #FFB020;border-radius:8px;padding:12px;background:#2A2410;color:#FFD580;">' +
|
||||
(before ? 'Für diese Antwort gibt es noch kein Protokoll (aufgezeichnet wird seit ' + escapeHtml(j.logSince) + ').'
|
||||
: '<b>Keine Befehle ausgeführt.</b> ARIA hat für diese Antwort nichts nachgesehen (kein Bash/ssh, kein Lesen). ' +
|
||||
'Sagt sie darin „nachgesehen" oder nennt sie einen aktuellen Stand, stammt das aus dem Verlauf oder ihrem Gedächtnis.') +
|
||||
'</div><div style="font-size:11px;color:#555570;margin-top:8px;">Nicht enthalten: ARIAs eigene Werkzeuge wie Gedächtnis-Suche oder Timer — die stehen im Brain-Log.</div>';
|
||||
return;
|
||||
}
|
||||
const prettyInput = (name, input) => {
|
||||
try { const o = JSON.parse(input); if (name === 'Bash' && o.command) return o.command;
|
||||
if (o.file_path) return o.file_path + (o.pattern ? ' · ' + o.pattern : '');
|
||||
if (o.pattern) return o.pattern + (o.path ? ' in ' + o.path : '');
|
||||
return JSON.stringify(o, null, 1); } catch { return input || ''; }
|
||||
};
|
||||
body.innerHTML = head + j.requests.map(r => r.tools.map(x =>
|
||||
'<div style="border:1px solid #1E1E2E;border-radius:8px;padding:8px 10px;margin-bottom:8px;background:#12122A;">' +
|
||||
'<div style="display:flex;justify-content:space-between;gap:8px;"><b style="color:' + (x.isError ? '#FF6B6B' : '#4DB8FF') + ';">' + escapeHtml(x.name || '?') + (x.isError ? ' · Fehler' : '') + '</b>' +
|
||||
'<span style="color:#555570;font-size:11px;">' + t(x.ts) + '</span></div>' +
|
||||
'<pre style="margin:6px 0 0;white-space:pre-wrap;word-break:break-all;background:#000;border-radius:6px;padding:6px 8px;color:#E0E0F0;font-size:12px;">' + escapeHtml(prettyInput(x.name, x.input)) + '</pre>' +
|
||||
(x.result != null ? '<details style="margin-top:6px;"><summary style="cursor:pointer;color:#8888AA;font-size:12px;">Ausgabe' +
|
||||
(x.resultTruncatedBytes ? ' (gekürzt, ' + x.resultTruncatedBytes + ' Zeichen mehr)' : '') + '</summary>' +
|
||||
'<pre style="margin:4px 0 0;white-space:pre-wrap;word-break:break-all;background:#000;border-radius:6px;padding:6px 8px;color:#3FFF9F;font-size:11px;max-height:320px;overflow:auto;">' + escapeHtml(x.result) + '</pre></details>'
|
||||
: '<div style="color:#8888AA;font-size:11px;margin-top:4px;">(keine Ausgabe protokolliert)</div>') +
|
||||
'</div>').join('')).join('') +
|
||||
'<div style="font-size:11px;color:#555570;">Passwörter/Tokens sind im Protokoll geschwärzt. Nicht enthalten: Gedächtnis-Suche/Timer (Brain-Werkzeuge).</div>';
|
||||
}
|
||||
|
||||
function openCtxInfo() { document.getElementById('ctx-info-modal').style.display = 'flex'; }
|
||||
|
||||
// Kontextfenster einer Karte aendern = dasselbe Modell mit anderem -c neu
|
||||
|
||||
@@ -554,6 +554,52 @@ async function refreshLlmCatalogFromHF() {
|
||||
return { models: merged, added };
|
||||
}
|
||||
|
||||
// ── Belege: Werkzeug-Aufrufe zu einer ARIA-Antwort ─────────────────
|
||||
const ARIA_TOOL_LOG_DIR = "/shared/logs/aria-tools";
|
||||
function ariaProof(toTs, project) {
|
||||
// Beginn = Stefans letzte Nachricht davor im selben Kontext (sonst 30 min)
|
||||
let fromTs = toTs - 30 * 60 * 1000;
|
||||
try {
|
||||
const lines = fs.readFileSync("/shared/config/chat_backup.jsonl", "utf-8").split("\n");
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (!lines[i]) continue;
|
||||
let e; try { e = JSON.parse(lines[i]); } catch { continue; }
|
||||
if (!e || !(e.ts < toTs) || e.role !== "user") continue;
|
||||
if ((e.project_id || "") !== project) continue;
|
||||
fromTs = Math.max(fromTs, e.ts - 2000);
|
||||
break;
|
||||
}
|
||||
} catch (_) { }
|
||||
const days = new Set([new Date(fromTs).toISOString().slice(0, 10), new Date(toTs).toISOString().slice(0, 10)]);
|
||||
const reqs = new Map();
|
||||
for (const d of days) {
|
||||
let txt = "";
|
||||
try { txt = fs.readFileSync(`${ARIA_TOOL_LOG_DIR}/${d}.jsonl`, "utf-8"); } catch { continue; }
|
||||
for (const line of txt.split("\n")) {
|
||||
if (!line) continue;
|
||||
let ev; try { ev = JSON.parse(line); } catch { continue; }
|
||||
if (!ev.requestId) continue;
|
||||
const r = reqs.get(ev.requestId) || { requestId: ev.requestId, project: null, start: 0, end: 0, tools: [], byId: {} };
|
||||
if (ev.kind === "start") { r.start = ev.ts; r.project = ev.projectId || ""; r.model = ev.model || ""; }
|
||||
else if (ev.kind === "end") r.end = ev.ts;
|
||||
else if (ev.kind === "tool_use") {
|
||||
const t = { id: ev.id, name: ev.name, input: ev.input, inputTruncatedBytes: ev.inputTruncatedBytes || 0, ts: ev.ts };
|
||||
r.tools.push(t); if (ev.id) r.byId[ev.id] = t;
|
||||
} else if (ev.kind === "tool_result") {
|
||||
const t = ev.id && r.byId[ev.id];
|
||||
if (t) { t.result = ev.content; t.isError = !!ev.isError; t.resultTruncatedBytes = ev.truncatedBytes || 0; t.doneTs = ev.ts; }
|
||||
}
|
||||
reqs.set(ev.requestId, r);
|
||||
}
|
||||
}
|
||||
const hit = [...reqs.values()]
|
||||
.filter(r => r.start && r.project === project && r.start >= fromTs && r.start <= toTs + 5000)
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.map(({ byId, ...r }) => r);
|
||||
const logSince = (() => { try { return fs.readdirSync(ARIA_TOOL_LOG_DIR).filter(f => /\.jsonl$/.test(f)).sort()[0]?.slice(0, 10) || ""; } catch { return ""; } })();
|
||||
return { from: fromTs, to: toTs, requests: hit, toolCount: hit.reduce((n, r) => n + r.tools.length, 0), logSince };
|
||||
}
|
||||
|
||||
// ── Gerade im Trend: ladbare GGUF-Modelle von HuggingFace ───────────
|
||||
// Fuer das Fenster "Bestenlisten" im Modell-Katalog. "Trend" ist nicht
|
||||
// "das Beste" — viele Community-Mischungen. Darum standardmaessig nur
|
||||
@@ -2388,6 +2434,17 @@ const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message, models: loadLlmCatalog() }));
|
||||
});
|
||||
} else if (req.url.startsWith("/api/aria-proof") && req.method === "GET") {
|
||||
// Belege zu einer ARIA-Antwort: alle Werkzeug-Aufrufe (Bash/ssh, Read, …),
|
||||
// die ARIA im Proxy zwischen Stefans Frage und dieser Antwort gemacht hat.
|
||||
// Quelle: /shared/logs/aria-tools/<tag>.jsonl (proxy-patches/routes.js).
|
||||
const u = new URL(req.url, "http://x");
|
||||
const to = Number(u.searchParams.get("ts")) || 0;
|
||||
const project = u.searchParams.get("project") || "";
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
if (!to) { res.end(JSON.stringify({ ok: false, error: "ts fehlt" })); return; }
|
||||
try { res.end(JSON.stringify({ ok: true, ...ariaProof(to, project) })); }
|
||||
catch (e) { res.end(JSON.stringify({ ok: false, error: e.message })); }
|
||||
} else if (req.url.startsWith("/api/llm-discover") && req.method === "GET") {
|
||||
const u = new URL(req.url, "http://x");
|
||||
discoverGguf(u.searchParams.get("cat") || "chat", u.searchParams.get("trusted") !== "0")
|
||||
|
||||
+45
-1
@@ -118,7 +118,51 @@ function _emitToolEvent(toolName, projectId) {
|
||||
* kind: "start" | "text" | "tool_use" | "tool_result" | "end"
|
||||
*/
|
||||
function _emitStreamEvent(requestId, kind, fields) {
|
||||
_postJson(STREAM_HOOK_URL, { requestId, kind, ts: Date.now(), ...fields });
|
||||
const ev = { requestId, kind, ts: Date.now(), ...fields };
|
||||
_postJson(STREAM_HOOK_URL, ev);
|
||||
_persistToolEvent(ev);
|
||||
}
|
||||
|
||||
// ── Werkzeug-Protokoll (Beleg) ────────────────────────────────
|
||||
// Was ARIA SELBST im Proxy ausfuehrt (Bash/ssh, Read, Edit, …) war bisher
|
||||
// nur live sichtbar. Jetzt zusaetzlich dauerhaft: eine JSONL-Datei pro Tag
|
||||
// unter /shared/logs/aria-tools/. Die Diagnostic zeigt pro ARIA-Antwort die
|
||||
// zugehoerigen Aufrufe ("Belege"). Nur start/tool_use/tool_result/end —
|
||||
// keine Text-Deltas. Fail-open: ein Schreibfehler stoert ARIA nie.
|
||||
const TOOL_LOG_DIR = process.env.ARIA_TOOL_LOG_DIR || "/shared/logs/aria-tools";
|
||||
const TOOL_LOG_KINDS = new Set(["start", "tool_use", "tool_result", "end"]);
|
||||
const TOOL_LOG_KEEP_DAYS = 30;
|
||||
let _toolLogCleaned = "";
|
||||
// Offensichtliche Geheimnisse nicht dauerhaft ablegen (Passwoerter, Tokens,
|
||||
// API-Keys, private Schluessel) — Ausgaben wie "cat .env" landen sonst im Log.
|
||||
const SECRET_PATTERNS = [
|
||||
[/(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)/g, "$1 *** $2"],
|
||||
[/((?:pass(?:word|wd)?|token|secret|api[_-]?key|auth|credential)[\w-]*["']?\s*[=:]\s*["']?)[^\s"',;]{4,}/gi, "$1***"],
|
||||
[/\b(hf_|ghp_|gho_|github_pat_|sk-|xox[bp]-|glpat-)[A-Za-z0-9_-]{8,}/g, "$1***"],
|
||||
[/(https?:\/\/[^\s:@\/]+:)[^\s@\/]+@/g, "$1***@"],
|
||||
];
|
||||
function _redact(v) {
|
||||
if (typeof v !== "string") return v;
|
||||
for (const [re, rep] of SECRET_PATTERNS) v = v.replace(re, rep);
|
||||
return v;
|
||||
}
|
||||
function _persistToolEvent(ev) {
|
||||
if (!TOOL_LOG_KINDS.has(ev.kind)) return;
|
||||
try {
|
||||
ev = { ...ev, input: _redact(ev.input), content: _redact(ev.content) };
|
||||
const day = new Date(ev.ts).toISOString().slice(0, 10);
|
||||
fs.mkdirSync(TOOL_LOG_DIR, { recursive: true });
|
||||
fs.appendFile(`${TOOL_LOG_DIR}/${day}.jsonl`, JSON.stringify(ev) + "\n", () => {});
|
||||
if (_toolLogCleaned !== day) { // einmal am Tag: alte Tage weg
|
||||
_toolLogCleaned = day;
|
||||
const cutoff = new Date(Date.now() - TOOL_LOG_KEEP_DAYS * 86400000).toISOString().slice(0, 10);
|
||||
for (const f of fs.readdirSync(TOOL_LOG_DIR)) {
|
||||
if (/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f) && f.slice(0, 10) < cutoff) {
|
||||
try { fs.unlinkSync(`${TOOL_LOG_DIR}/${f}`); } catch (_) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) { /* fail-open */ }
|
||||
}
|
||||
|
||||
function _truncate(str, max) {
|
||||
|
||||
Reference in New Issue
Block a user