mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 05:50:44 +08:00
Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards. Co-authored-by: Cursor <cursoragent@cursor.com>
416 lines
20 KiB
JavaScript
416 lines
20 KiB
JavaScript
import { t, el, tdCell, apiGet, formatIds, yesNo } from "../core.js";
|
|
|
|
async function renderAudit(initialSessionId = "") {
|
|
const input = el("input", { class: "input", placeholder: t("audit.sessionIdPlaceholder") });
|
|
const resultWrap = el("div");
|
|
let selectedTraceId = "";
|
|
const onlyFailures = el("input", { type: "checkbox" });
|
|
const formatTracePayload = (row) => {
|
|
const et = String((row && row.event_type) || "");
|
|
const p = (row && typeof row.payload === "object" && row.payload) ? row.payload : {};
|
|
if (et === "tool_wire_filter") {
|
|
const before = Number(p.tools_before || 0);
|
|
const after = Number(p.tools_after || 0);
|
|
const hidden = Number(p.hidden_total || 0);
|
|
const hiddenMcp = Number(p.hidden_mcp_total || 0);
|
|
const preview = Array.isArray(p.hidden_mcp_preview) ? p.hidden_mcp_preview.slice(0, 8).join(", ") : "";
|
|
const head = `wire_filter before=${before} after=${after} hidden=${hidden} hidden_mcp=${hiddenMcp}`;
|
|
return preview ? `${head} | mcp: ${preview}` : head;
|
|
}
|
|
if (et === "llm_first_token") {
|
|
const ms = Number(p.first_token_ms || 0);
|
|
return `ttft_ms=${ms}`;
|
|
}
|
|
if (et === "memory_retrieval_finished") {
|
|
return `memory short=${Number(p.short_term_count || 0)} semantic=${Number(p.semantic_hit_count || 0)} enabled=${Boolean(p.enabled)}`;
|
|
}
|
|
if (et === "router_decision") {
|
|
return `route mode=${String(p.mode || "")} reason=${String(p.reason || "")}`;
|
|
}
|
|
if (et === "run_started") {
|
|
return `run_started run_id=${String(p.run_id || "")} max_attempts=${Number(p.max_attempts || 0)}`;
|
|
}
|
|
if (et === "attempt_started") {
|
|
return `attempt_started run_id=${String(p.run_id || "")} no=${Number(p.attempt_no || 0)}`;
|
|
}
|
|
if (et === "attempt_finished") {
|
|
return `attempt_finished run_id=${String(p.run_id || "")} no=${Number(p.attempt_no || 0)} status=${String(p.status || "")}`;
|
|
}
|
|
if (et === "run_retry") {
|
|
return `run_retry run_id=${String(p.run_id || "")} next_attempt=${Number(p.next_attempt_no || 0)}`;
|
|
}
|
|
if (et === "run_compact") {
|
|
return `run_compact run_id=${String(p.run_id || "")} count=${Number(p.compact_count || 0)}`;
|
|
}
|
|
if (et === "run_finished") {
|
|
return `run_finished run_id=${String(p.run_id || "")} status=${String(p.status || "")} attempts=${Number(p.attempts || 0)}`;
|
|
}
|
|
if (et === "task_enqueued" || et === "task_claimed" || et === "task_finished" || et === "task_failed") {
|
|
return `${et} task_id=${String(p.task_id || "")} ok=${p.ok === undefined ? "-" : Boolean(p.ok)}`;
|
|
}
|
|
if (et === "turn_stage" && String(p.stage || "") === "finish") {
|
|
const ttft = p.first_token_ms;
|
|
const elapsed = Number(p.turn_elapsed_ms || 0);
|
|
const tools = Number(p.tool_trace_count || 0);
|
|
return `finish ttft_ms=${ttft === null || ttft === undefined ? "-" : Number(ttft)} elapsed_ms=${elapsed} tools=${tools}`;
|
|
}
|
|
return JSON.stringify(p || {});
|
|
};
|
|
const auditColumnDefs = [
|
|
{ key: "timestamp", label: t("table.timestamp"), getter: (r) => r.timestamp || "", maxLen: 24 },
|
|
{ key: "specialist", label: t("table.specialist"), getter: (r) => r.specialist || "", maxLen: 24 },
|
|
{ key: "action", label: t("table.action"), getter: (r) => r.action || "", maxLen: 24 },
|
|
{ key: "status", label: t("table.statusText"), getter: (r) => r.status || "", maxLen: 16 },
|
|
{ key: "reason", label: t("table.reason"), getter: (r) => r.reason || "", maxLen: 40 },
|
|
{ key: "duration", label: t("table.durationMs"), getter: (r) => String(r.duration_ms || ""), maxLen: 20 },
|
|
{
|
|
key: "noOutput",
|
|
label: t("table.noOutputAttachment"),
|
|
getter: (r) => yesNo(((r.payload && typeof r.payload === "object") ? r.payload.no_output_attachment : false)),
|
|
maxLen: 8,
|
|
},
|
|
{
|
|
key: "attachments",
|
|
label: "attachments",
|
|
getter: (r) => {
|
|
const payload = (r && typeof r.payload === "object" && r.payload) ? r.payload : {};
|
|
const routeIds = formatIds(payload.attachment_ids);
|
|
const inIds = formatIds(payload.input_attachment_ids);
|
|
const outIds = formatIds(payload.output_attachment_ids);
|
|
const outUrls = formatIds(payload.output_attachment_urls);
|
|
return [routeIds ? `route:${routeIds}` : "", inIds ? `in:${inIds}` : "", outIds ? `out:${outIds}` : "", outUrls ? `url:${outUrls}` : ""]
|
|
.filter(Boolean)
|
|
.join(" | ") || "-";
|
|
},
|
|
maxLen: 120,
|
|
},
|
|
{
|
|
key: "payload",
|
|
label: t("table.payload"),
|
|
getter: (r) => JSON.stringify(r.payload || {}),
|
|
maxLen: 120,
|
|
},
|
|
];
|
|
const traceColumnDefs = [
|
|
{ key: "timestamp", label: t("table.timestamp"), getter: (r) => r.timestamp || "", maxLen: 24 },
|
|
{ key: "eventType", label: t("table.eventType"), getter: (r) => r.event_type || "", maxLen: 28 },
|
|
{ key: "traceId", label: t("table.traceId"), getter: (r) => r.trace_id || "", maxLen: 20 },
|
|
{ key: "spanId", label: t("table.spanId"), getter: (r) => r.span_id || "", maxLen: 20 },
|
|
{ key: "parentSpanId", label: t("table.parentSpanId"), getter: (r) => r.parent_span_id || "", maxLen: 20 },
|
|
{
|
|
key: "errorCode",
|
|
label: "error_code",
|
|
getter: (r) => String((r.payload && r.payload.error_code) || ""),
|
|
maxLen: 32,
|
|
},
|
|
{ key: "payload", label: t("table.payload"), getter: (r) => formatTracePayload(r), maxLen: 120 },
|
|
];
|
|
const auditVisible = new Set(auditColumnDefs.map((x) => x.key));
|
|
const traceVisible = new Set(traceColumnDefs.map((x) => x.key));
|
|
|
|
const buildColumnToggleRow = (defs, visibleSet) => {
|
|
const wrap = el("div", { class: "row" });
|
|
defs.forEach((d) => {
|
|
const cb = el("input", { type: "checkbox" });
|
|
cb.checked = visibleSet.has(d.key);
|
|
cb.addEventListener("change", () => {
|
|
if (cb.checked) visibleSet.add(d.key);
|
|
else visibleSet.delete(d.key);
|
|
});
|
|
wrap.appendChild(el("label", { class: "kv" }, [cb, document.createTextNode(" " + d.label)]));
|
|
});
|
|
return wrap;
|
|
};
|
|
|
|
const auditColsToggle = buildColumnToggleRow(auditColumnDefs, auditVisible);
|
|
const traceColsToggle = buildColumnToggleRow(traceColumnDefs, traceVisible);
|
|
const runQuery = async () => {
|
|
const sid = input.value.trim();
|
|
const a = await apiGet("/admin/api/audit" + (sid ? ("?session_id=" + encodeURIComponent(sid)) : ""));
|
|
const tr = await apiGet("/admin/api/trace" + (sid ? ("?session_id=" + encodeURIComponent(sid)) : ""));
|
|
const tasks = await apiGet("/admin/api/oclaw/tasks?" + (sid ? ("session_id=" + encodeURIComponent(sid) + "&") : "") + "limit=120");
|
|
const runs = await apiGet("/admin/api/oclaw/runs?" + (sid ? ("session_id=" + encodeURIComponent(sid) + "&") : "") + "limit=80&include_attempts=1");
|
|
const health = await apiGet(
|
|
"/admin/api/audit/session-health?" + (sid ? ("session_id=" + encodeURIComponent(sid) + "&limit=20") : "limit=40"),
|
|
);
|
|
const auditRows = Array.isArray(a.audit) ? a.audit : [];
|
|
const traceRows = Array.isArray(tr.trace) ? tr.trace : [];
|
|
const traceIds = Array.from(new Set(traceRows.map((r) => String(r.trace_id || "")).filter(Boolean)));
|
|
if (selectedTraceId && !traceIds.includes(selectedTraceId)) selectedTraceId = "";
|
|
const filteredByTrace = selectedTraceId ? traceRows.filter((r) => String(r.trace_id || "") === selectedTraceId) : traceRows;
|
|
const filteredTraceRows = onlyFailures.checked
|
|
? filteredByTrace.filter((r) => {
|
|
const p = (r && typeof r.payload === "object") ? r.payload : {};
|
|
return p.ok === false || String(p.error_code || "").trim() !== "";
|
|
})
|
|
: filteredByTrace;
|
|
const auditBody = el("tbody");
|
|
const visibleAuditCols = auditColumnDefs.filter((c) => auditVisible.has(c.key));
|
|
if (!auditRows.length) {
|
|
auditBody.appendChild(el("tr", {}, [el("td", { text: t("audit.empty"), colspan: String(Math.max(1, visibleAuditCols.length)) })]));
|
|
} else {
|
|
auditRows.forEach((r) => {
|
|
const row = el("tr");
|
|
visibleAuditCols.forEach((c) => row.appendChild(tdCell(c.getter(r), c.maxLen || 120)));
|
|
auditBody.appendChild(row);
|
|
});
|
|
}
|
|
const traceBody = el("tbody");
|
|
const visibleTraceCols = traceColumnDefs.filter((c) => traceVisible.has(c.key));
|
|
if (!filteredTraceRows.length) {
|
|
traceBody.appendChild(el("tr", {}, [el("td", { text: t("audit.empty"), colspan: String(Math.max(1, visibleTraceCols.length)) })]));
|
|
} else {
|
|
filteredTraceRows.forEach((r) => {
|
|
const row = el("tr");
|
|
visibleTraceCols.forEach((c) => row.appendChild(tdCell(c.getter(r), c.maxLen || 120)));
|
|
traceBody.appendChild(row);
|
|
});
|
|
}
|
|
const pre = el("div", { class: "pre", text: JSON.stringify({ audit: a, trace: tr, health }, null, 2) });
|
|
resultWrap.innerHTML = "";
|
|
const healthItems = Array.isArray(health.items) ? health.items : [];
|
|
const healthRows = healthItems.map((x) => el("tr", {}, [
|
|
tdCell(String(x.session_id || ""), 26),
|
|
tdCell(String(x.title || ""), 26),
|
|
tdCell(String(x.assistant_count || 0), 8),
|
|
tdCell(String(x.tool_count || 0), 8),
|
|
tdCell(String(x.mcp_tool_count || 0), 8),
|
|
tdCell(String(x.last_tool_at || "-"), 20),
|
|
tdCell(String(x.status || ""), 20),
|
|
]));
|
|
resultWrap.appendChild(el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: "Session Tool Health" }),
|
|
el("div", { class: "muted", text: `warn=${Number(health.warn_count || 0)} (assistant replied but no tool uses)` }),
|
|
el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [el("tr", {}, [
|
|
el("th", { text: "session_id" }),
|
|
el("th", { text: "title" }),
|
|
el("th", { text: "assistant_msgs" }),
|
|
el("th", { text: "tool_uses" }),
|
|
el("th", { text: "mcp_calls" }),
|
|
el("th", { text: "last_tool_at" }),
|
|
el("th", { text: "status" }),
|
|
])]),
|
|
el("tbody", {}, healthRows.length ? healthRows : [el("tr", {}, [el("td", { text: "-", colspan: "7" })])]),
|
|
])]),
|
|
]));
|
|
const taskRows = Array.isArray(tasks.tasks) ? tasks.tasks : [];
|
|
const queued = Number((tasks.counts || {}).queued || 0);
|
|
const claimed = Number((tasks.counts || {}).claimed || 0);
|
|
const done = Number((tasks.counts || {}).done || 0);
|
|
const failed = Number((tasks.counts || {}).failed || 0);
|
|
resultWrap.appendChild(el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: "oclaw Tasks" }),
|
|
el("div", { class: "muted", text: `queued=${queued} claimed=${claimed} done=${done} failed=${failed}` }),
|
|
el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [el("tr", {}, [
|
|
el("th", { text: "task_id" }),
|
|
el("th", { text: "status" }),
|
|
el("th", { text: "attempt_count" }),
|
|
el("th", { text: "session_id" }),
|
|
el("th", { text: "updated_at" }),
|
|
el("th", { text: "last_error" }),
|
|
])]),
|
|
el(
|
|
"tbody",
|
|
{},
|
|
taskRows.length
|
|
? taskRows.slice(0, 80).map((x) => el("tr", {}, [
|
|
tdCell(String(x.id || ""), 24),
|
|
tdCell(String(x.status || ""), 10),
|
|
tdCell(String(x.attempt_count || 0), 10),
|
|
tdCell(String(x.session_id || ""), 20),
|
|
tdCell(String(x.updated_at || ""), 22),
|
|
tdCell(String(x.last_error || ""), 40),
|
|
]))
|
|
: [el("tr", {}, [el("td", { text: "-", colspan: "6" })])],
|
|
),
|
|
])]),
|
|
]));
|
|
const runRows = Array.isArray(runs.runs) ? runs.runs : [];
|
|
const runRunning = Number((runs.counts || {}).running || 0);
|
|
const runSuccess = Number((runs.counts || {}).success || 0);
|
|
const runFailed = Number((runs.counts || {}).failed || 0);
|
|
const rp = (runs && typeof runs.retry_policy === "object" && runs.retry_policy) ? runs.retry_policy : {};
|
|
const effRetryCodes = Array.isArray(rp.effective_retryable_error_codes) ? rp.effective_retryable_error_codes.join(", ") : "";
|
|
resultWrap.appendChild(el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: "oclaw Runs" }),
|
|
el("div", { class: "muted", text: `running=${runRunning} success=${runSuccess} failed=${runFailed}` }),
|
|
el("div", { class: "muted", text: effRetryCodes ? `retryable_error_codes=${effRetryCodes}` : "" }),
|
|
el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [el("tr", {}, [
|
|
el("th", { text: "run_id" }),
|
|
el("th", { text: "status" }),
|
|
el("th", { text: "session_id" }),
|
|
el("th", { text: "attempts" }),
|
|
el("th", { text: "updated_at" }),
|
|
])]),
|
|
el(
|
|
"tbody",
|
|
{},
|
|
runRows.length
|
|
? runRows.slice(0, 60).map((x) => {
|
|
const attempts = Array.isArray(x.attempts) ? x.attempts : [];
|
|
const attemptSummary = attempts.length
|
|
? attempts.map((a) => `${Number(a.attempt_no || 0)}:${String(a.status || "")}`).join(" | ")
|
|
: "-";
|
|
return el("tr", {}, [
|
|
tdCell(String(x.run_id || ""), 24),
|
|
tdCell(String(x.status || ""), 10),
|
|
tdCell(String(x.session_id || ""), 20),
|
|
tdCell(attemptSummary, 60),
|
|
tdCell(String(x.updated_at || ""), 22),
|
|
]);
|
|
})
|
|
: [el("tr", {}, [el("td", { text: "-", colspan: "5" })])],
|
|
),
|
|
])]),
|
|
]));
|
|
|
|
const deliveryRows = auditRows
|
|
.filter((r) => r && typeof r === "object" && r.action === "specialist_step")
|
|
.map((r) => {
|
|
const payload = (r && typeof r.payload === "object" && r.payload) ? r.payload : {};
|
|
const d = (payload && typeof payload.specialist_delivery === "object" && payload.specialist_delivery)
|
|
? payload.specialist_delivery
|
|
: null;
|
|
if (!d) return null;
|
|
const traces = Array.isArray(d.tool_traces) ? d.tool_traces : [];
|
|
const toolsText = traces.length
|
|
? traces
|
|
.map((x) => `${String(x.name || "")}(ok=${Boolean(x.ok)}, ${Number(x.latency_ms || 0)}ms)`)
|
|
.join(" | ")
|
|
: "-";
|
|
return {
|
|
timestamp: String(r.timestamp || ""),
|
|
specialist: String(r.specialist || d.specialist || ""),
|
|
stepId: String(d.step_id || payload.step_id || ""),
|
|
answer: String(d.answer_text || ""),
|
|
tools: toolsText,
|
|
notes: String(d.notes || ""),
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
resultWrap.appendChild(el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: "Specialist Delivery Timeline" }),
|
|
el("div", { class: "muted", text: t("audit.specialistSummaryHint") }),
|
|
el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [el("tr", {}, [
|
|
el("th", { text: "timestamp" }),
|
|
el("th", { text: "specialist" }),
|
|
el("th", { text: "step_id" }),
|
|
el("th", { text: "answer_for_user" }),
|
|
el("th", { text: "tools_executed_inside_specialist" }),
|
|
el("th", { text: "notes" }),
|
|
])]),
|
|
el(
|
|
"tbody",
|
|
{},
|
|
deliveryRows.length
|
|
? deliveryRows.map((x) =>
|
|
el("tr", {}, [
|
|
tdCell(x.timestamp, 24),
|
|
tdCell(x.specialist, 14),
|
|
tdCell(x.stepId, 20),
|
|
tdCell(x.answer, 120),
|
|
tdCell(x.tools, 120),
|
|
tdCell(x.notes, 24),
|
|
]),
|
|
)
|
|
: [el("tr", {}, [el("td", { text: "-", colspan: "6" })])],
|
|
),
|
|
])]),
|
|
]));
|
|
|
|
// Turn summary (group by trace_id) for quick navigation.
|
|
if (traceIds.length) {
|
|
const sel = el("select", { class: "input" });
|
|
sel.appendChild(el("option", { value: "", text: `trace_id (${traceIds.length})` }));
|
|
traceIds.forEach((tid) => sel.appendChild(el("option", { value: tid, text: tid })));
|
|
sel.value = selectedTraceId;
|
|
const summary = el("div", { class: "muted", text: "" });
|
|
const failSummary = el("div", { class: "muted", text: "" });
|
|
const recomputeSummary = () => {
|
|
const rows = filteredTraceRows;
|
|
const counts = {};
|
|
rows.forEach((r) => {
|
|
const et = String(r.event_type || "");
|
|
counts[et] = (counts[et] || 0) + 1;
|
|
});
|
|
const get = (k) => Number(counts[k] || 0);
|
|
summary.textContent = [
|
|
`route_decided=${get("route_decided")}`,
|
|
`plan_created=${get("plan_created")}`,
|
|
`tool_wire_filter=${get("tool_wire_filter")}`,
|
|
`llm_first_token=${get("llm_first_token")}`,
|
|
`llm_called=${get("llm_called")}`,
|
|
`llm_result=${get("llm_result")}`,
|
|
`tool_use_called=${get("tool_called")}`,
|
|
`tool_result=${get("tool_result")}`,
|
|
].join(" | ");
|
|
const agg = {};
|
|
rows.forEach((r) => {
|
|
const p = (r && typeof r.payload === "object") ? r.payload : {};
|
|
const ec = String(p.error_code || "").trim();
|
|
if (!ec) return;
|
|
agg[ec] = (agg[ec] || 0) + 1;
|
|
});
|
|
const parts = Object.entries(agg).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([k, v]) => `${k}:${v}`);
|
|
failSummary.textContent = parts.length ? `Failure summary: ${parts.join(" | ")}` : "Failure summary: -";
|
|
};
|
|
sel.addEventListener("change", () => {
|
|
selectedTraceId = String(sel.value || "");
|
|
runQuery();
|
|
});
|
|
recomputeSummary();
|
|
resultWrap.appendChild(el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: "Turn summary" }),
|
|
el("div", { class: "row" }, [sel, el("label", { class: "kv" }, [onlyFailures, document.createTextNode(" only failures")])]),
|
|
summary,
|
|
failSummary,
|
|
]));
|
|
}
|
|
resultWrap.appendChild(el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: t("audit.auditTable") }),
|
|
el("div", { class: "muted", text: t("audit.columns") }),
|
|
auditColsToggle,
|
|
el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [el("tr", {}, auditColumnDefs.filter((c) => auditVisible.has(c.key)).map((c) => el("th", { text: c.label })))]),
|
|
auditBody,
|
|
])]),
|
|
]));
|
|
resultWrap.appendChild(el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: t("audit.traceTable") }),
|
|
el("div", { class: "muted", text: t("audit.columns") }),
|
|
traceColsToggle,
|
|
el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [el("tr", {}, traceColumnDefs.filter((c) => traceVisible.has(c.key)).map((c) => el("th", { text: c.label })))]),
|
|
traceBody,
|
|
])]),
|
|
]));
|
|
const details = el("details", { class: "details" }, [
|
|
el("summary", { text: t("audit.rawJson") }),
|
|
pre,
|
|
]);
|
|
resultWrap.appendChild(el("div", { class: "card" }, [details]));
|
|
};
|
|
const btn = el("button", { class: "btn btn--primary", text: t("audit.query"), onclick: async () => {
|
|
await runQuery();
|
|
}});
|
|
onlyFailures.addEventListener("change", runQuery);
|
|
auditColsToggle.addEventListener("change", runQuery);
|
|
traceColsToggle.addEventListener("change", runQuery);
|
|
const card = el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: t("audit.title") }),
|
|
el("div", { class: "row" }, [input, btn]),
|
|
el("div", { class: "muted", text: t("audit.note") }),
|
|
]);
|
|
if (initialSessionId) {
|
|
input.value = initialSessionId;
|
|
await runQuery();
|
|
}
|
|
return el("div", {}, [card, resultWrap]);
|
|
}
|
|
|
|
|
|
export { renderAudit };
|