feat(web): surface Workbench diagnostics

This commit is contained in:
lyon
2026-06-20 16:03:05 +08:00
parent cf219821f0
commit d947b9e4e8
5 changed files with 315 additions and 26 deletions
@@ -158,8 +158,8 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
if (traceMatch && method === "GET") {
const traceId = decodeURIComponent(traceMatch[1] ?? "");
if (state.scenarioId === "completed-replay-detail-404" && traceId === "trc_completed") return json(response, 404, { ok: false, status: 404, error: { code: "trace_replay_unavailable" } });
if (state.scenarioId === "trace-hydration-timeout-diagnostic" && traceId === "trc_trace_hydration_timeout") return json(response, 504, { ok: false, status: 504, error: { code: "trace_hydration_timeout", message: "Trace 更新超时,运行记录暂不可见。" } });
if (state.scenarioId === "sealed-final-response-diagnostics" && traceId === "trc_sealed_final_diag") return json(response, 504, { ok: false, status: 504, error: { code: "trace_hydration_timeout", message: "Trace 更新超时,运行记录暂不可见。" } });
if (state.scenarioId === "trace-hydration-timeout-diagnostic" && traceId === "trc_trace_hydration_timeout") return errorDiagnosticResponse(response, 504, `/v1/workbench/traces/${traceId}/events`, "trace_hydration_timeout", "Trace 更新超时,运行记录暂不可见。", { traceId: "33333333333333333333333333333333", requestId: "req_e2e_trace_hydration_timeout", layer: "workbench-read-model", category: "trace-hydration" });
if (state.scenarioId === "sealed-final-response-diagnostics" && traceId === "trc_sealed_final_diag") return errorDiagnosticResponse(response, 504, `/v1/workbench/traces/${traceId}/events`, "trace_hydration_timeout", "Trace 更新超时,运行记录暂不可见。", { traceId: "55555555555555555555555555555555", requestId: "req_e2e_sealed_final_trace_timeout", layer: "workbench-read-model", category: "trace-hydration" });
if (traceId === state.staleTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
return json(response, 200, workbenchTracePayload(traceId, url));
}
@@ -921,6 +921,20 @@ function toolCompletedProjectionRunningTrace(): JsonRecord {
}
function projectionDiagnostic(input: { code: string; message: string; health?: string; category?: string; layer?: string; traceId: string; runId?: string; commandId?: string; staleMs?: number }): JsonRecord {
const diagnostic = {
contractVersion: "hwlab-error-diagnostic-v1",
traceId: input.traceId,
requestId: `req_e2e_${input.code}`,
route: "/v1/workbench/events",
layer: input.layer ?? "agentrun",
category: input.category ?? "upstream-timeout",
code: input.code,
httpStatus: 0,
source: "server",
observedAt: new Date().toISOString(),
retryable: true,
valuesPrinted: false
};
return {
projectionStatus: "projecting",
projectionHealth: input.health ?? "degraded",
@@ -937,8 +951,15 @@ function projectionDiagnostic(input: { code: string; message: string; health?: s
timeoutMs: 2500,
runId: input.runId ?? "run_projection_diag",
commandId: input.commandId ?? "cmd_projection_diag",
traceId: diagnostic.traceId,
requestId: diagnostic.requestId,
route: diagnostic.route,
source: diagnostic.source,
httpStatus: diagnostic.httpStatus,
diagnostic,
valuesPrinted: false
},
diagnostic,
valuesRedacted: true
};
}
@@ -1884,17 +1905,17 @@ async function readJson(request: IncomingMessage): Promise<JsonRecord> {
}
}
function errorDiagnosticResponse(response: ServerResponse, status: number, route: string, code: string, message: string): void {
function errorDiagnosticResponse(response: ServerResponse, status: number, route: string, code: string, message: string, overrides: Partial<JsonRecord> = {}): void {
const diagnostic = {
contractVersion: "hwlab-error-diagnostic-v1",
traceId: "11111111111111111111111111111111",
requestId: "req_e2e_api_error_diagnostic",
traceId: String(overrides.traceId ?? "11111111111111111111111111111111"),
requestId: String(overrides.requestId ?? "req_e2e_api_error_diagnostic"),
route,
layer: "api",
category: "server",
layer: String(overrides.layer ?? "api"),
category: String(overrides.category ?? "server"),
code,
httpStatus: status,
source: "server",
source: String(overrides.source ?? "server"),
observedAt: new Date().toISOString(),
valuesPrinted: false
};