diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index d2ea11b0..b7591911 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -815,6 +815,7 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) }); if ((runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.requestFailedCount, groups: runtimeAlerts.networkRequestFailedByPath.slice(0, 12) }); if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, samples: runtimeAlerts.domDiagnostics.slice(0, 12) }); + if ((runtimeAlerts?.summary?.executionErrorCount ?? 0) > 0) findings.push({ id: "runtime-execution-errors", severity: "red", summary: "Workbench rendered execution failure/error rows during observation", count: runtimeAlerts.summary.executionErrorCount, groups: runtimeAlerts.runtimeExecutionErrorsByCode.slice(0, 12) }); if ((runtimeAlerts?.summary?.consoleAlertCount ?? 0) > 0) findings.push({ id: "runtime-console-alerts", severity: "amber", summary: "browser console warning/error entries were captured during observation", count: runtimeAlerts.summary.consoleAlertCount, groups: runtimeAlerts.consoleAlertsByPath.slice(0, 12) }); const naturalApi = network.filter((item) => item.observerInitiated === false && item.type === "response" && /\/v1\/|\/auth\//u.test(String(item.url || ""))); findings.push({ id: "natural-api-dom-lag-baseline", severity: "info", summary: "natural API responses and DOM samples are available for manual lag correlation", naturalApiResponses: naturalApi.length, sampleCount: samples.length }); @@ -925,6 +926,7 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { .filter((item) => item?.type === "requestfailed") .map((item) => networkAlertEvent(item, promptTimes)); const domDiagnostics = []; + const executionErrors = []; for (const sample of samples) { const tsMs = Date.parse(sample?.ts); const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; @@ -964,6 +966,43 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { preview: limitText(text, 220) }); } + const seenExecutionErrors = new Set(); + for (const candidate of sampleExecutionErrorCandidates(sample)) { + const parsed = parseExecutionErrorText(candidate.text); + if (!parsed) continue; + const dedupeKey = [candidate.source, candidate.traceId || "-", parsed.backend || "-", parsed.code || "-", parsed.status || "-", sha256(candidate.text)].join("|"); + if (seenExecutionErrors.has(dedupeKey)) continue; + seenExecutionErrors.add(dedupeKey); + executionErrors.push({ + seq: sample.seq ?? null, + ts: sample.ts ?? null, + promptIndex, + source: candidate.source, + backend: parsed.backend, + status: parsed.status, + code: parsed.code, + rawCode: parsed.rawCode, + totalSeconds: parsed.totalSeconds, + traceId: candidate.traceId || parsed.traceId || null, + messageId: candidate.messageId || null, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + textHash: sha256(candidate.text), + preview: limitText(candidate.text, 260) + }); + domDiagnostics.push({ + seq: sample.seq ?? null, + ts: sample.ts ?? null, + promptIndex, + source: "execution-row", + diagnosticCode: parsed.rawCode || parsed.code || "execution-error", + traceId: candidate.traceId || parsed.traceId || null, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + textHash: sha256(candidate.text), + preview: limitText(candidate.text, 220) + }); + } } const consoleAlerts = consoleEvents .filter((item) => /error|warning|warn|assert/iu.test(String(item?.type || "")) || isDiagnosticText(item?.text)) @@ -981,21 +1020,110 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { httpErrorCount: httpErrors.length, requestFailedCount: requestFailed.length, domDiagnosticSampleCount: domDiagnostics.length, + executionErrorCount: executionErrors.length, consoleAlertCount: consoleAlerts.length, pageErrorCount: pageErrors.length, networkErrorGroupCount: groupNetworkAlerts(httpErrors).length, requestFailedGroupCount: groupNetworkAlerts(requestFailed).length, + executionErrorGroupCount: groupExecutionErrors(executionErrors).length, consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length }, networkHttpErrorsByPath: groupNetworkAlerts(httpErrors), networkRequestFailedByPath: groupNetworkAlerts(requestFailed), domDiagnostics: domDiagnostics.slice(0, 80), + runtimeExecutionErrors: executionErrors.slice(0, 120), + runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors), consoleAlerts: consoleAlerts.slice(0, 80), consoleAlertsByPath: groupConsoleAlerts(consoleAlerts), pageErrors: pageErrors.slice(0, 40) }; } +function sampleExecutionErrorCandidates(sample) { + const candidates = []; + const add = (source, items) => { + if (!Array.isArray(items)) return; + for (const item of items) { + const text = String(item?.textPreview || item?.text || item?.preview || "").trim(); + if (!text) continue; + if (!parseExecutionErrorText(text)) continue; + candidates.push({ + source, + text, + traceId: item?.traceId ?? null, + messageId: item?.messageId ?? null, + status: item?.status ?? null + }); + } + }; + add("diagnostic-node", sample?.diagnostics); + add("message", sample?.messages); + add("trace-row", sample?.traceRows); + add("turn", sample?.turns); + return candidates; +} + +function parseExecutionErrorText(text) { + const value = String(text || ""); + const agentRunCodeMatch = value.match(/\bagentrun:error:([a-z0-9_.:-]+)/iu); + const agentRunText = /\bAgentRun\s+error\b|\bagentrun:error:/iu.test(value); + const providerUnavailable = /\bprovider[-_\s]*unavailable\b/iu.test(value); + if (!agentRunCodeMatch && !agentRunText && !providerUnavailable) return null; + const statusMatch = value.match(/\b(fail(?:ed)?|error|blocked|cancel(?:ed)?)\b/iu); + const traceMatch = value.match(/\btrc_[A-Za-z0-9_-]+\b/u); + const totalMatch = value.match(/\btotal\s*=\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)\b/iu) + || value.match(/总耗时\s*[::]?\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)/iu); + const rawCode = agentRunCodeMatch ? "agentrun:error:" + agentRunCodeMatch[1] : providerUnavailable ? "provider-unavailable" : "agentrun:error"; + return { + backend: agentRunText || agentRunCodeMatch ? "agentrun" : "unknown", + status: normalizeExecutionStatus(statusMatch?.[1] || "error"), + code: agentRunCodeMatch?.[1] || (providerUnavailable ? "provider-unavailable" : "error"), + rawCode, + totalSeconds: totalMatch ? parseClockDurationSeconds(totalMatch[1]) : null, + traceId: traceMatch?.[0] || null + }; +} + +function normalizeExecutionStatus(status) { + const value = String(status || "").toLowerCase(); + if (value === "failed") return "fail"; + if (value === "cancelled" || value === "canceled") return "cancel"; + return value || "error"; +} + +function parseClockDurationSeconds(value) { + const parts = String(value || "").split(":").map((part) => Number(part)); + if (parts.length === 2 && parts.every(Number.isFinite)) return parts[0] * 60 + parts[1]; + if (parts.length === 3 && parts.every(Number.isFinite)) return parts[0] * 3600 + parts[1] * 60 + parts[2]; + return null; +} + +function groupExecutionErrors(events) { + const groups = new Map(); + for (const event of events) { + const key = [event.backend || "-", event.status || "-", event.code || "-"].join(" "); + const group = groups.get(key) || { + backend: event.backend ?? null, + status: event.status ?? null, + code: event.code ?? null, + rawCode: event.rawCode ?? null, + count: 0, + firstAt: event.ts, + lastAt: event.ts, + promptIndexes: [], + traceIds: [], + sources: [] + }; + group.count += 1; + group.lastAt = event.ts; + if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); + if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId); + if (event.source && !group.sources.includes(event.source)) group.sources.push(event.source); + groups.set(key, group); + } + return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.code).localeCompare(String(b.code))); +} + function consoleAlertEvent(item, promptTimes) { const text = String(item?.text || ""); const statusMatch = text.match(/\bstatus\s+of\s+([1-5][0-9]{2})\b/iu) || text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); @@ -1074,7 +1202,7 @@ function groupNetworkAlerts(events) { } function isDiagnosticText(text) { - return /Failed to fetch|request failed|realtime-gap|durable projection store|turn 超过|无法连接|暂时|报警|告警|警告|失败|错误|异常|超时|timeout|error|failed|warning|502|503|504|provider-unavailable|projection-resume/iu.test(String(text || "")); + return /Failed to fetch|request failed|realtime-gap|durable projection store|turn 超过|无法连接|暂时|报警|告警|警告|失败|错误|异常|超时|timeout|error|failed|warning|502|503|504|provider-unavailable|agentrun:error|AgentRun error|projection-resume/iu.test(String(text || "")); } function buildSampleMetrics(samples, control) {