feat(monitor): surface workbench sentinel root causes

This commit is contained in:
Codex
2026-06-27 01:52:50 +00:00
parent 3e4320eb88
commit 631e8ad23e
9 changed files with 382 additions and 17 deletions
@@ -1084,6 +1084,24 @@ select {
overflow-wrap: anywhere;
}
.finding-root-cause {
display: grid;
gap: 3px;
padding: 7px 8px;
border-left: 3px solid #2563eb;
border-radius: 4px;
background: #f5f9ff;
color: #1f2937;
font-size: 12px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.finding-root-cause strong {
color: #1d4ed8;
font-size: 11px;
}
.finding-actions {
display: flex;
flex-wrap: wrap;
@@ -523,6 +523,9 @@ function renderFindingItemCollapsed(item) {
const latestRunId = item.latestRunId || "-";
const hasLatestRun = latestRunId !== "-";
const codeLabel = displayFindingCode(code);
const rootCause = findingRootCauseText(item);
const evidence = findingEvidenceText(item);
const nextAction = item.nextAction || findingNextAction(code);
return `<article class="finding-item is-collapsed" data-finding-run-id="${escapeAttr(latestRunId)}">
<div class="finding-row" data-finding-toggle>
<span class="finding-title">${escapeHtml(codeLabel)}${codeLabel === code ? "" : ` <small class="mono">${escapeHtml(code)}</small>`}<small> · ${formatNumber(item.count ?? 0)} 次</small></span>
@@ -536,7 +539,9 @@ function renderFindingItemCollapsed(item) {
</div>
<div class="finding-meta">次数=${escapeHtml(String(item.count ?? 0))} · 运行=${escapeHtml(String(item.runCount ?? 0))} · 最近=${escapeHtml(item.latestAt ? formatRelative(item.latestAt) : "-")}</div>
<div class="finding-meta mono">run=${escapeHtml(latestRunId)} report=${escapeHtml(item.latestReportJsonSha256 || "-")}</div>
<div class="finding-meta">${escapeHtml(findingNextAction(code))}</div>
${rootCause ? `<div class="finding-root-cause"><strong>根因</strong><span>${escapeHtml(rootCause)}</span></div>` : ""}
${evidence ? `<div class="finding-meta mono">evidence=${escapeHtml(evidence)}</div>` : ""}
<div class="finding-meta">${escapeHtml(nextAction)}</div>
<button type="button" class="link-button" data-open-finding-run="${escapeAttr(latestRunId)}"${hasLatestRun ? "" : " disabled"}>打开最近运行</button>
</div>
</article>`;
@@ -732,12 +737,13 @@ function detailFindings(findings) {
}
return `<article class="detail-block"><strong>运行发现项 · ${formatNumber(findings.length)} 条</strong>
<div class="detail-table-frame"><table class="detail-table"><thead><tr>
<th>严重级别</th><th>代码</th><th>次数</th><th>摘要</th><th>报告</th>
<th>严重级别</th><th>代码</th><th>次数</th><th>摘要</th><th>根因</th><th>报告</th>
</tr></thead><tbody>${findings.map((item) => `<tr>
<td><span class="severity-pill ${severityClass(item.severity)}">${escapeHtml(displaySeverity(item.severity))}</span></td>
<td class="mono">${escapeHtml(item.finding_id || item.findingId || "-")}</td>
<td>${escapeHtml(String(item.count ?? 0))}</td>
<td>${escapeHtml(shortText(displayFindingSummary(item.finding_id || item.findingId || "", item.summary || ""), 220))}</td>
<td>${escapeHtml(shortText(findingRootCauseText(item) || item.nextAction || "-", 240))}</td>
<td class="mono">${escapeHtml(shortText(item.report_json_sha256 || item.reportJsonSha256 || "-", 24))}</td>
</tr>`).join("")}</tbody></table></div>
</article>`;
@@ -1252,6 +1258,10 @@ function displayFindingCode(code) {
const normalized = String(code || "").toLowerCase();
const labels = {
"quick-verify-no-business-turn": "quick verify 未触达业务 turn",
"session-rail-title-fallback-root-cause": "INV-02 会话标题 fallback 根因",
"trace-events-page-read-404-root-cause": "INV-07 trace events 404 根因",
"trace-events-page-read-http-error-root-cause": "trace events HTTP 错误根因",
"trace-events-page-read-requestfailed-root-cause": "trace events 网络失败根因",
"observer-command-failed": "观察器控制命令失败",
"runtime-requestfailed": "运行时请求失败",
"runtime-console-alerts": "运行时控制台告警",
@@ -1267,6 +1277,10 @@ function displayFindingSummary(code, summary) {
const normalized = String(code || "").toLowerCase();
const summaries = {
"quick-verify-no-business-turn": "quick verify 没有形成 sendPrompt、session、trace rows 或 Final Response;不能把公开 dashboard 200 当作 HWLAB 恢复证据。",
"session-rail-title-fallback-root-cause": "会话列表可见 Session ses_* fallback 标题;根因定位到 session list projection/read model 或 rail 数据绑定缺稳定 title/preview。",
"trace-events-page-read-404-root-cause": "trace events page read 返回 404;根因定位到 /v1/workbench/traces/:traceId/events API 分页/read-model 合约,早于 DOM 渲染。",
"trace-events-page-read-http-error-root-cause": "trace events page read 返回 HTTP 错误;根因定位到 trace-events API 路径,不是通用前端渲染失败。",
"trace-events-page-read-requestfailed-root-cause": "trace events page read 在浏览器网络层失败;monitor 已定位路径,但需要 OTel/API 字段确认后端根因。",
"observer-command-failed": "observe control command 失败,需要查看 observer timeline 和 failed command 文件确认是 readiness、session API、超时还是 runner shutdown。",
"runtime-requestfailed": "页面运行时存在请求失败,需要按路径聚合确认是 asset/provenance 噪声、public origin、auth/session 还是 Workbench API。",
"runtime-console-alerts": "页面控制台出现告警,需要结合 run detail 的 network/console 证据判断是否影响业务 turn。",
@@ -1284,6 +1298,10 @@ function findingNextAction(code) {
const normalized = String(code || "").toLowerCase();
const actions = {
"quick-verify-no-business-turn": "下一步: 打开该 run 的 turn-summary/trace-frame,并用 CLI 对照命令确认没有业务 turn。",
"session-rail-title-fallback-root-cause": "下一步: 查同 run 的 OTel session_list_read fallbackTitleCount/fallbackTitleRatio,修 session list projection/read model title/preview。",
"trace-events-page-read-404-root-cause": "下一步: 查同 trace 的 OTel trace_events_read sinceSeq/range/hasMore/fullTraceLoaded,修 trace-events 分页合约或 instrumentation。",
"trace-events-page-read-http-error-root-cause": "下一步: 按 traceId/afterProjectedSeq 对齐 OTel trace_events_read 和 HTTP route span。",
"trace-events-page-read-requestfailed-root-cause": "下一步: 先排除 observer refresh/navigation;若非刷新噪声,补 HTTP route span 的 traceId/status/afterProjectedSeq 字段。",
"observer-command-failed": "下一步: 查看 observe collect timeline 和 failed command 文件,定位失败阶段。",
"runtime-requestfailed": "下一步: 按请求路径聚合失败,区分网络、auth/session、API 或静态资源问题。",
"runtime-console-alerts": "下一步: 结合 console 样本与业务 trace 判断是否为阻塞级。",
@@ -1294,6 +1312,23 @@ function findingNextAction(code) {
return actions[normalized] || "下一步: 打开最近运行详情,并用 CLI 对照命令复核同一 run/observer/report。";
}
function findingRootCauseText(item) {
const rootCause = item.rootCause || "";
const status = item.rootCauseStatus || "";
const confidence = item.rootCauseConfidence || "";
if (!rootCause && !status && !confidence) return "";
return [
rootCause ? `rootCause=${rootCause}` : "",
status ? `status=${status}` : "",
confidence ? `confidence=${confidence}` : "",
].filter(Boolean).join(" · ");
}
function findingEvidenceText(item) {
const text = item.evidenceSummary || "";
return text ? shortText(String(text), 220) : "";
}
function formatSeveritySummary(counts) {
const entries = Object.entries(counts || {}).filter(([, value]) => Number(value || 0) > 0);
if (entries.length === 0) return "无";