From 45f890bf7f5211740263ac8e112e9cb4cb416dca Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 30 Jun 2026 20:31:07 +0000 Subject: [PATCH] fix: harden sentinel quick verify findings --- .../monitor-web.js | 28 ++++- .../src/hwlab-node-web-sentinel-p5-observe.ts | 109 +++++++++++++++++- 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js index d1b3682a..37228fa1 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -1187,17 +1187,37 @@ function safeDetailValue(value) { function checkDisplay(item) { const rawCode = rawCheckCode(item); - const registered = checkDisplayCatalog[rawCode]; - if (registered) return registered; + const rawId = rawFindingId(item); + const registered = checkDisplayCatalog[rawId] || checkDisplayCatalog[rawCode]; + const serviceCode = displayCheckCode(item?.checkCode || item?.check?.code); + if (registered) { + return { + ...registered, + code: serviceCode || registered.code, + title: safeUserText(item?.checkTitleZh || item?.check?.titleZh) || registered.title, + summary: safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary || item?.check?.summaryZh) || registered.summary, + }; + } return { - code: stableCheckCode(rawCode), + code: serviceCode || displayCheckCode(rawId || rawCode) || stableCheckCode(rawCode), title: safeUserText(item?.checkTitleZh || item?.check?.titleZh) || "未登记监测项", summary: safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary) || "已记录监测项详情,见报告原文。", }; } function rawCheckCode(item) { - return String(item?.checkCode || item?.check?.code || item?.code || item?.findingId || item?.kind || "unknown"); + return String(item?.checkCode || item?.check?.code || rawFindingId(item) || "unknown"); +} + +function rawFindingId(item) { + const value = item?.findingId || item?.finding_id || item?.id || item?.kind || item?.code; + return value === null || value === undefined || value === "" ? "" : String(value); +} + +function displayCheckCode(value) { + const text = String(value || "").replace(/\s+/g, " ").trim(); + if (text.length === 0 || text === "unknown") return ""; + return text; } function stableCheckCode(value) { diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts index 4bdd475f..ba612ae7 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts @@ -941,18 +941,49 @@ function collectObserveView(state: SentinelCicdState, observerId: string, view: if (turn !== null) args.push("--turn", String(turn)); const result = runChildCli(args, timeoutSeconds); const payload = cliDataPayload(result.parsed); - const collect = record(payload.collect); + const collect = observeCollectPayload(payload); + const payloadData = record(payload.data); return { ok: result.ok && result.parsed !== null && payload.ok !== false && collect.ok !== false, view, - renderedText: typeof collect.renderedText === "string" ? collect.renderedText : typeof payload.renderedText === "string" ? payload.renderedText : String(record(result.result).stdoutTail ?? record(result.result).stdoutPreview ?? ""), + renderedText: typeof collect.renderedText === "string" + ? collect.renderedText + : typeof payload.renderedText === "string" + ? payload.renderedText + : typeof payloadData.renderedText === "string" + ? payloadData.renderedText + : String(record(result.result).stdoutTail ?? record(result.result).stdoutPreview ?? ""), collect, payload, + collectShape: observeCollectShape(payload, collect), result: result.result, valuesRedacted: true, }; } +function observeCollectPayload(payload: Record): Record { + const candidates = [ + record(payload.collect), + record(record(payload.data).collect), + record(record(record(payload.data).data).collect), + ]; + return candidates.find((item) => Object.keys(item).length > 0) ?? {}; +} + +function observeCollectShape(payload: Record, collect: Record): Record { + const direct = record(payload.collect); + const nested = record(record(payload.data).collect); + return { + payloadKeys: Object.keys(payload).slice(0, 12), + directCollect: Object.keys(direct).length > 0, + nestedDataCollect: Object.keys(nested).length > 0, + collectKeys: Object.keys(collect).slice(0, 12), + rowCount: Array.isArray(collect.rows) ? collect.rows.length : null, + traceId: stringAtNullable(collect, "traceId"), + valuesRedacted: true, + }; +} + export function runChildCli(args: string[], timeoutSeconds: number, input?: string, env?: NodeJS.ProcessEnv): ChildCliResult { const result = runCommand(["bun", "scripts/cli.ts", ...args], repoRoot, { input, @@ -1397,6 +1428,8 @@ function observerCommandFailureBlocks(item: Record): boolean { function quickVerifyControlFindings(failure: string | null, promptIndex: number, turnSummary: Record | null, traceFrame: Record | null): Record[] { if (quickVerifyHasDurableBusinessTurn(promptIndex, turnSummary, traceFrame)) return []; + const turnDiagnostics = quickVerifyTurnSummaryDiagnostics(promptIndex, turnSummary); + const traceDiagnostics = quickVerifyTraceFrameDiagnostics(traceFrame); const rendered = [ typeof turnSummary?.renderedText === "string" ? turnSummary.renderedText : "", typeof traceFrame?.renderedText === "string" ? traceFrame.renderedText : "", @@ -1425,6 +1458,13 @@ function quickVerifyControlFindings(failure: string | null, promptIndex: number, severity: "red", count: 1, summary: "quick verify did not reach a durable business turn/session/trace rows/final response; public dashboard health cannot be treated as HWLAB recovery.", + rootCause: `quick verify could not confirm a durable completed turn: turn-summary scopedRows=${String(turnDiagnostics.scopedRowCount ?? 0)} rowCount=${String(turnDiagnostics.rowCount ?? 0)}, traceFrame traceIdPresent=${traceDiagnostics.traceIdPresent === true} finalResponseEmpty=${traceDiagnostics.finalResponseEmpty === true}.`, + rootCauseStatus: "confirmed", + rootCauseConfidence: "high", + evidenceSummary: `turn-summary rows=${String(turnDiagnostics.rowCount ?? 0)} scoped=${String(turnDiagnostics.scopedRowCount ?? 0)} traceFrameTrace=${traceDiagnostics.traceIdPresent === true ? "present" : "missing"} finalResponseBytes=${String(traceDiagnostics.finalResponseBytes ?? "-")}`, + nextAction: "Inspect the structured turnSummary/traceFrame diagnostics first; if rows exist with completed non-empty final responses, fix sentinel interpretation instead of treating HWLAB Web as blocked.", + turnSummaryDiagnostics: turnDiagnostics, + traceFrameDiagnostics: traceDiagnostics, failure: failure ?? null, promptIndex, valuesRedacted: true, @@ -1458,7 +1498,7 @@ function enrichObserveStartFailureFinding(finding: Record, resu } function quickVerifyCompletedTurnSummaryRow(promptIndex: number, turnSummary: Record | null): Record | null { - const rows = Array.isArray(record(turnSummary?.collect).rows) ? record(turnSummary?.collect).rows.map(record) : []; + const rows = quickVerifyTurnSummaryRows(turnSummary); const scopedRows = promptIndex > 0 ? rows.filter((row) => numberAtNullable(row, "round") === promptIndex) : rows; return scopedRows.find((row) => { const finalResponse = record(row.finalResponse); @@ -1471,7 +1511,7 @@ function quickVerifyCompletedTurnSummaryRow(promptIndex: number, turnSummary: Re function quickVerifyTurnSummaryFallback(state: SentinelCicdState, observerId: string, promptIndex: number): Record { const turnSummary = collectObserveView(state, observerId, "turn-summary", null, 25); const row = quickVerifyCompletedTurnSummaryRow(promptIndex, turnSummary); - const rows = Array.isArray(record(turnSummary.collect).rows) ? record(turnSummary.collect).rows.map(record) : []; + const rows = quickVerifyTurnSummaryRows(turnSummary); if (row === null) { return { ok: false, @@ -1479,6 +1519,7 @@ function quickVerifyTurnSummaryFallback(state: SentinelCicdState, observerId: st collectOk: turnSummary.ok === true, rowCount: rows.length, promptIndex, + diagnostics: quickVerifyTurnSummaryDiagnostics(promptIndex, turnSummary), result: turnSummary.result ?? null, valuesRedacted: true, }; @@ -1501,6 +1542,7 @@ function quickVerifyTurnSummaryFallback(state: SentinelCicdState, observerId: st function quickVerifyHasDurableBusinessTurn(promptIndex: number, turnSummary: Record | null, traceFrame: Record | null): boolean { if (quickVerifyCompletedTurnSummaryRow(promptIndex, turnSummary) !== null) return true; + if (quickVerifyTraceFrameHasDurableTurn(traceFrame)) return true; const renderedTrace = typeof traceFrame?.renderedText === "string" ? traceFrame.renderedText : ""; if (!renderedTrace) return false; if (/Final Response\s*\n\s*\(空内容\)/iu.test(renderedTrace)) return false; @@ -1509,6 +1551,65 @@ function quickVerifyHasDurableBusinessTurn(promptIndex: number, turnSummary: Rec && !/无\s*trace\s*rows|no\s+trace\s+rows|traceId=-|routeSession=-|activeSession=-/iu.test(renderedTrace); } +function quickVerifyTurnSummaryRows(turnSummary: Record | null): Record[] { + const collect = record(turnSummary?.collect); + return Array.isArray(collect.rows) ? collect.rows.map(record) : []; +} + +function quickVerifyTraceFrameHasDurableTurn(traceFrame: Record | null): boolean { + const collect = record(traceFrame?.collect); + if (collect.ok === false) return false; + if (stringAtNullable(collect, "traceId") === null) return false; + const finalResponse = record(collect.finalResponse); + if (finalResponse.empty === true) return false; + const textBytes = numberAtNullable(finalResponse, "textBytes"); + return textBytes === null || textBytes > 0; +} + +function quickVerifyTraceFrameDiagnostics(traceFrame: Record | null): Record { + const collect = record(traceFrame?.collect); + const finalResponse = record(collect.finalResponse); + return { + collectOk: traceFrame?.ok === true, + collectShape: record(traceFrame?.collectShape), + collectView: stringAtNullable(collect, "view"), + traceIdPresent: stringAtNullable(collect, "traceId") !== null, + finalResponseEmpty: finalResponse.empty === true, + finalResponseBytes: numberAtNullable(finalResponse, "textBytes"), + blockerPresent: collect.blocker !== null && collect.blocker !== undefined, + result: traceFrame?.result ?? null, + valuesRedacted: true, + }; +} + +function quickVerifyTurnSummaryDiagnostics(promptIndex: number, turnSummary: Record | null): Record { + const collect = record(turnSummary?.collect); + const rows = quickVerifyTurnSummaryRows(turnSummary); + const scopedRows = promptIndex > 0 ? rows.filter((row) => numberAtNullable(row, "round") === promptIndex) : rows; + const scopedStatuses = scopedRows.slice(0, 4).map((row) => { + const finalResponse = record(row.finalResponse); + return { + round: numberAtNullable(row, "round"), + status: stringAtNullable(row, "status"), + traceIdPresent: stringAtNullable(row, "traceId") !== null, + finalResponseEmpty: finalResponse.empty === true, + finalResponseBytes: numberAtNullable(finalResponse, "textBytes"), + valuesRedacted: true, + }; + }); + return { + collectOk: turnSummary?.ok === true, + collectShape: record(turnSummary?.collectShape), + collectView: stringAtNullable(collect, "view"), + rowCount: rows.length, + scopedRowCount: scopedRows.length, + promptIndex, + scopedStatuses, + result: turnSummary?.result ?? null, + valuesRedacted: true, + }; +} + function quickVerifyBusinessStatus( failure: string | null, promptIndex: number,