// SPEC: PJ2026-01040111 long-running Workbench observation. // Responsibility: Analyzer request-rate, prompt network, runtime console/network alert, and diagnostic grouping source fragment. export function nodeWebObserveAnalyzerRequestRuntimeSource(): string { return String.raw`function buildRequestRateReport(network) { const bucketMs = Math.max(1000, Number(alertThresholds.requestRateBucketMs || 0)); const naturalApiRequests = (Array.isArray(network) ? network : []) .filter((item) => item?.observerInitiated !== true && item?.type === "request") .map((item) => requestRateEvent(item)) .filter((item) => item !== null) .sort((a, b) => a.tsMs - b.tsMs); if (naturalApiRequests.length === 0) { return { summary: { bucketMs, bucketSeconds: Number((bucketMs / 1000).toFixed(3)), requestCount: 0, bucketCount: 0, pageCount: 0, apiPathCount: 0, totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute, pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute, apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute, totalPeakPerMinute: 0, pagePeakPerMinute: 0, apiPathPeakPerMinute: 0, overThresholdPeakCount: 0, valuesRedacted: true, }, buckets: [], pageCurves: [], apiPathCurves: [], peaks: [], valuesRedacted: true, }; } const firstBucketMs = Math.floor(naturalApiRequests[0].tsMs / bucketMs) * bucketMs; const lastBucketMs = Math.floor(naturalApiRequests[naturalApiRequests.length - 1].tsMs / bucketMs) * bucketMs; const makeBucket = (bucketStartMs, extra = {}) => ({ bucketStartMs, bucketEndMs: bucketStartMs + bucketMs, startAt: new Date(bucketStartMs).toISOString(), endAt: new Date(bucketStartMs + bucketMs).toISOString(), count: 0, requestPerMinute: 0, ...extra, valuesRedacted: true, }); const totalBuckets = new Map(); const pageBuckets = new Map(); const apiPathBuckets = new Map(); const pageTotals = new Map(); const apiPathTotals = new Map(); const allBucketStarts = []; for (let bucketStartMs = firstBucketMs; bucketStartMs <= lastBucketMs; bucketStartMs += bucketMs) { totalBuckets.set(bucketStartMs, makeBucket(bucketStartMs)); allBucketStarts.push(bucketStartMs); } for (const item of naturalApiRequests) { const bucketStartMs = Math.floor(item.tsMs / bucketMs) * bucketMs; const totalBucket = totalBuckets.get(bucketStartMs) || makeBucket(bucketStartMs); totalBucket.count += 1; totalBuckets.set(bucketStartMs, totalBucket); const pageKey = item.pageKey; const pageMapKey = pageKey + "|" + bucketStartMs; const pageBucket = pageBuckets.get(pageMapKey) || makeBucket(bucketStartMs, { pageKey, pageRole: item.pageRole, pageId: item.pageId, pageEpoch: item.pageEpoch, path: item.framePath, }); pageBucket.count += 1; pageBuckets.set(pageMapKey, pageBucket); const pageTotal = pageTotals.get(pageKey) || { pageKey, pageRole: item.pageRole, pageId: item.pageId, pageEpoch: item.pageEpoch, path: item.framePath, count: 0 }; pageTotal.count += 1; pageTotals.set(pageKey, pageTotal); const apiKey = item.method + " " + item.apiPath; const apiMapKey = apiKey + "|" + bucketStartMs; const apiBucket = apiPathBuckets.get(apiMapKey) || makeBucket(bucketStartMs, { apiKey, method: item.method, path: item.apiPath, pageKey, pageRole: item.pageRole, pageId: item.pageId, pageEpoch: item.pageEpoch, }); apiBucket.count += 1; apiPathBuckets.set(apiMapKey, apiBucket); const apiTotal = apiPathTotals.get(apiKey) || { apiKey, method: item.method, path: item.apiPath, count: 0 }; apiTotal.count += 1; apiPathTotals.set(apiKey, apiTotal); } const finalizeBucket = (bucket) => ({ ...bucket, requestPerMinute: Number((Number(bucket.count || 0) * 60000 / bucketMs).toFixed(2)), }); const buckets = Array.from(totalBuckets.values()).map(finalizeBucket); const pageBucketRows = Array.from(pageBuckets.values()).map(finalizeBucket); const apiPathBucketRows = Array.from(apiPathBuckets.values()).map(finalizeBucket); const peakForRows = (rows, thresholdPerMinute, scope, extra = {}) => rows .filter((row) => Number(row.requestPerMinute ?? 0) >= Number(thresholdPerMinute || Infinity)) .map((row) => ({ scope, thresholdPerMinute, overThreshold: true, bucketMs, bucketStartMs: row.bucketStartMs, bucketEndMs: row.bucketEndMs, startAt: row.startAt, endAt: row.endAt, count: row.count, requestPerMinute: row.requestPerMinute, ...extra(row), valuesRedacted: true, })); const totalPeaks = peakForRows(buckets, alertThresholds.requestRateTotalRedPerMinute, "total", () => ({})); const pagePeaks = peakForRows(pageBucketRows, alertThresholds.requestRatePageRedPerMinute, "page", (row) => ({ pageKey: row.pageKey, pageRole: row.pageRole, pageId: row.pageId, pageEpoch: row.pageEpoch, path: row.path, })); const apiPathPeaks = peakForRows(apiPathBucketRows, alertThresholds.requestRateApiPathRedPerMinute, "apiPath", (row) => ({ apiKey: row.apiKey, method: row.method, path: row.path, pageKey: row.pageKey, pageRole: row.pageRole, pageId: row.pageId, pageEpoch: row.pageEpoch, })); const pageCurves = Array.from(pageTotals.values()) .map((page) => { const rows = pageBucketRows .filter((row) => row.pageKey === page.pageKey) .sort((a, b) => a.bucketStartMs - b.bucketStartMs); const peak = maxByNumber(rows, (row) => row.requestPerMinute); return { ...page, bucketCount: rows.length, peakRequestPerMinute: peak?.requestPerMinute ?? 0, peakBucket: peak ?? null, buckets: rows.slice(-60), valuesRedacted: true, }; }) .sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0)); const apiPathCurves = Array.from(apiPathTotals.values()) .map((api) => { const rows = apiPathBucketRows .filter((row) => row.apiKey === api.apiKey) .sort((a, b) => a.bucketStartMs - b.bucketStartMs); const peak = maxByNumber(rows, (row) => row.requestPerMinute); return { ...api, bucketCount: rows.length, peakRequestPerMinute: peak?.requestPerMinute ?? 0, peakBucket: peak ?? null, buckets: rows.slice(-60), valuesRedacted: true, }; }) .sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0)); const totalPeak = maxByNumber(buckets, (row) => row.requestPerMinute); const pagePeak = pageCurves[0] ?? null; const apiPathPeak = apiPathCurves[0] ?? null; const peaks = [...totalPeaks, ...pagePeaks, ...apiPathPeaks] .sort((a, b) => Number(b.requestPerMinute ?? 0) - Number(a.requestPerMinute ?? 0) || String(a.scope).localeCompare(String(b.scope))); return { summary: { bucketMs, bucketSeconds: Number((bucketMs / 1000).toFixed(3)), requestCount: naturalApiRequests.length, bucketCount: buckets.length, pageCount: pageCurves.length, apiPathCount: apiPathCurves.length, firstAt: new Date(firstBucketMs).toISOString(), lastAt: new Date(lastBucketMs + bucketMs).toISOString(), totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute, pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute, apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute, totalPeakPerMinute: totalPeak?.requestPerMinute ?? 0, totalPeakCount: totalPeak?.count ?? 0, totalPeakAt: totalPeak?.startAt ?? null, pagePeakPerMinute: pagePeak?.peakRequestPerMinute ?? 0, pagePeakKey: pagePeak?.pageKey ?? null, pagePeakPath: pagePeak?.path ?? null, apiPathPeakPerMinute: apiPathPeak?.peakRequestPerMinute ?? 0, apiPathPeakKey: apiPathPeak?.apiKey ?? null, overThresholdPeakCount: peaks.length, valuesRedacted: true, }, buckets: buckets.slice(-120), pageCurves: pageCurves.slice(0, 20), apiPathCurves: apiPathCurves.slice(0, 40), peaks: peaks.slice(0, 80), valuesRedacted: true, }; } function requestRateEvent(item) { const tsMs = Date.parse(String(item?.ts || "")); const apiPath = normalizeApiRatePath(urlPath(item?.url)); if (!Number.isFinite(tsMs) || !isApiLikePath(apiPath)) return null; const framePath = normalizeRoutePath(urlPath(item?.frameUrl || item?.url)); const pageRole = String(item?.pageRole || "unknown"); const pageId = String(item?.pageId || "unknown"); const pageEpoch = Number.isFinite(Number(item?.pageEpoch)) ? Number(item.pageEpoch) : null; return { ts: item.ts, tsMs, method: String(item?.method || "GET").toUpperCase(), apiPath, framePath, pageRole, pageId, pageEpoch, pageKey: [pageRole, pageId, pageEpoch ?? "-", framePath || "-"].join("|"), valuesRedacted: true, }; } function normalizeApiRatePath(value) { return normalizeRoutePath(value) .replace(/\/sessions\/[^/?#]+/gu, "/sessions/:id") .replace(/\/turns\/[^/?#]+/gu, "/turns/:id") .replace(/\/traces\/[^/?#]+/gu, "/traces/:id") .replace(/\/messages\/[^/?#]+/gu, "/messages/:id") .replace(/\/files\/[^/?#]+/gu, "/files/:id") .replace(/\/tasks\/[^/?#]+/gu, "/tasks/:id") .replace(/\/runs\/[^/?#]+/gu, "/runs/:id") .replace(/\/[0-9a-f]{8,}(?=\/|$)/giu, "/:id") .replace(/\/[A-Za-z0-9_-]{24,}(?=\/|$)/gu, "/:id"); } function normalizeRoutePath(value) { const raw = String(value || ""); if (!raw || raw === "-") return "-"; return raw.replace(/\/+/gu, "/").replace(/\/$/u, "") || "/"; } function compactRequestRateForOutput(report) { if (!report || typeof report !== "object") return null; return { summary: report.summary ?? null, buckets: Array.isArray(report.buckets) ? report.buckets.slice(-12) : [], pageCurves: Array.isArray(report.pageCurves) ? report.pageCurves.slice(0, 8).map((item) => ({ pageKey: item.pageKey ?? null, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, pageEpoch: item.pageEpoch ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-12) : [], valuesRedacted: true, })) : [], apiPathCurves: Array.isArray(report.apiPathCurves) ? report.apiPathCurves.slice(0, 12).map((item) => ({ apiKey: item.apiKey ?? null, method: item.method ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-12) : [], valuesRedacted: true, })) : [], peaks: Array.isArray(report.peaks) ? report.peaks.slice(0, 12) : [], valuesRedacted: true, }; } function buildPromptNetworkReport(control, network) { const promptsById = new Map(); for (const item of control) { if (item?.type !== "sendPrompt" || !item.commandId) continue; const existing = promptsById.get(item.commandId) || { commandId: item.commandId, promptIndex: promptsById.size + 1, promptTextHash: item.input?.textHash ?? null, promptTextBytes: item.input?.textBytes ?? null, startedAt: null, completedAt: null, failedAt: null, phase: null }; if (!existing.promptTextHash && item.input?.textHash) existing.promptTextHash = item.input.textHash; if (!existing.promptTextBytes && item.input?.textBytes) existing.promptTextBytes = item.input.textBytes; if (item.phase === "started") existing.startedAt = item.ts ?? existing.startedAt; if (item.phase === "completed") existing.completedAt = item.ts ?? existing.completedAt; if (item.phase === "failed") existing.failedAt = item.ts ?? existing.failedAt; existing.phase = item.phase ?? existing.phase; promptsById.set(item.commandId, existing); } const prompts = Array.from(promptsById.values()).sort((a, b) => Date.parse(a.startedAt || a.completedAt || a.failedAt || "") - Date.parse(b.startedAt || b.completedAt || b.failedAt || "")); prompts.forEach((item, index) => { item.promptIndex = index + 1; }); const chatEvents = network .filter((item) => String(item?.method || "").toUpperCase() === "POST" && promptSubmitModeForUrl(item?.url) !== null) .map((item) => { const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null; const urlPathValue = urlPath(item.url); return { ts: item.ts ?? null, tsMs: Date.parse(item.ts), type: item.type ?? null, status: Number.isFinite(Number(item.status)) ? Number(item.status) : null, commandId: item.commandId ?? null, urlPath: urlPathValue, submitMode: promptSubmitModeForUrl(item.url), failureKind: failureText ? String(failureText) : null, errorTextHash: failureText ? sha256(failureText) : null }; }) .filter((item) => Number.isFinite(item.tsMs)) .sort((a, b) => a.tsMs - b.tsMs); const rounds = prompts.map((prompt) => { const startMs = Date.parse(prompt.startedAt || prompt.completedAt || prompt.failedAt || ""); const endAnchorMs = Date.parse(prompt.completedAt || prompt.failedAt || prompt.startedAt || ""); const fromMs = Number.isFinite(startMs) ? startMs - 3000 : Number.NEGATIVE_INFINITY; const toMs = Number.isFinite(endAnchorMs) ? endAnchorMs + 30000 : Number.POSITIVE_INFINITY; const events = chatEvents.filter((event) => { if (event.commandId && prompt.commandId && event.commandId === prompt.commandId) return true; return event.tsMs >= fromMs && event.tsMs <= toMs; }); const responses = events.filter((event) => event.type === "response"); const failures = events.filter((event) => event.type === "requestfailed"); const responseStatuses = responses.map((event) => event.status).filter((status) => status !== null); const submitModes = Array.from(new Set(events.map((event) => event.submitMode).filter(Boolean))).sort(); const chatPostOk = responseStatuses.some((status) => status >= 200 && status < 300); const failureKind = chatPostOk ? null : failures.length > 0 ? "requestfailed" : responseStatuses.length === 0 ? "missing-response" : "http-status"; return { promptIndex: prompt.promptIndex, promptCommandId: prompt.commandId, promptTextHash: prompt.promptTextHash, promptTextBytes: prompt.promptTextBytes, startedAt: prompt.startedAt, completedAt: prompt.completedAt, failedAt: prompt.failedAt, chatPostOk, failureKind, requestCount: events.filter((event) => event.type === "request").length, responseCount: responses.length, requestFailedCount: failures.length, responseStatuses, submitModes, steerUsed: submitModes.includes("steer"), firstChatEventAt: events[0]?.ts ?? null, lastChatEventAt: events[events.length - 1]?.ts ?? null, events: events.slice(0, 12).map((event) => ({ ts: event.ts, type: event.type, status: event.status, urlPath: event.urlPath, submitMode: event.submitMode, failureKind: event.failureKind, errorTextHash: event.errorTextHash })) }; }); return { summary: { promptCount: rounds.length, chatPostOk: rounds.filter((item) => item.chatPostOk === true).length, chatPostFailed: rounds.filter((item) => item.chatPostOk === false).length, chatPostMissing: rounds.filter((item) => item.failureKind === "missing-response").length }, rounds }; } function promptSubmitModeForUrl(value) { const pathValue = urlPath(value); if (pathValue === "/v1/agent/chat") return "chat"; if (pathValue === "/v1/agent/chat/steer") return "steer"; return null; } function parseDomDiagnosticSummary(text) { const value = String(text || ""); const traceMatch = value.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu); const httpStatusMatch = value.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); const idleMatch = value.match(/\bidle\s+(\d+)s\b/iu); const waitingForMatch = value.match(/\bwaitingFor=([^\s;;,,)]+)/iu); const lastEventLabelMatch = value.match(/\blastEventLabel=([^\s;;,,)]+)/iu); const diagnosticCode = httpStatusMatch ? "http-" + httpStatusMatch[1] : /turn\s*超过|无新活动/iu.test(value) ? "turn-idle-no-activity" : /Failed to fetch/iu.test(value) ? "failed-to-fetch" : "diagnostic"; return { diagnosticCode, traceId: traceMatch?.[1] || null, httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null, idleSeconds: idleMatch ? Number(idleMatch[1]) : null, waitingFor: waitingForMatch?.[1] || null, lastEventLabel: lastEventLabelMatch?.[1] || null }; } function isDomDiagnosticSampleText(text) { const value = String(text || "").replace(/\s+/g, " ").trim(); if (!value) return false; const strongDiagnostic = [ /\bHTTP\s+[45][0-9]{2}\b(?:[\s\S]{0,120}\btrace_id=|\b)/iu, /\btrace_id=(?:trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu, /workbench\s+turn\s*超过\s*\d+ms\s*无新活动/iu, /\bturn\s*超过\b[\s\S]{0,120}\b无新活动\b/iu, /\bprojection-resume:sync-failed\b/iu, /\bAgentRun\s+GET\b[\s\S]*\/result\b[\s\S]*timed out after\s+\d+ms\b/iu, /\bFailed to fetch\b/iu, /\bFailed to load resource\b[\s\S]{0,180}\bstatus of\s+[45][0-9]{2}\b/iu, /\bserver responded with a status of\s+[45][0-9]{2}\b/iu, /Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束)/iu ].some((pattern) => pattern.test(value)); if (!strongDiagnostic) return false; const looksLikeToolStdout = /\b(?:stdout|stderr):/iu.test(value) && /(?:\becho\s+["']?===|\bnode\s+|\.tspy\b|tspy\/|===\s*[A-Za-z0-9_.-]+\s*===)/iu.test(value); if (!looksLikeToolStdout) return true; return /\b(?:trace_id=|HTTP\s+[45][0-9]{2}|workbench\s+turn\s*超过|projection-resume:sync-failed|Failed to fetch|Failed to load resource|server responded with a status of\s+[45][0-9]{2}|Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束))\b/iu.test(value); } function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { const promptTimes = control .filter((item) => item.type === "sendPrompt" && item.phase === "completed") .map((item) => Date.parse(item.ts)) .filter(Number.isFinite) .sort((a, b) => a - b); const observerRefreshTimes = control .filter((item) => item.type === "observer-periodic-refresh") .map((item) => Date.parse(item.ts)) .filter(Number.isFinite) .sort((a, b) => a - b); const naturalNetwork = network.filter((item) => item?.observerInitiated !== true); const httpErrors = naturalNetwork .filter((item) => item?.type === "response" && Number(item.status) >= 400) .map((item) => networkAlertEvent(item, promptTimes)); const requestFailed = naturalNetwork .filter((item) => item?.type === "requestfailed") .map((item) => networkAlertEvent(item, promptTimes)); const workbenchSessionListReadCount = naturalNetwork .filter((item) => urlPath(item?.url) === "/v1/workbench/sessions") .length; const workbenchTraceEventsReadCount = naturalNetwork .filter((item) => /^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath(item?.url))) .length; const webPerformanceBeaconFailureCount = naturalNetwork .filter((item) => urlPath(item?.url) === "/v1/web-performance" && (item?.type === "requestfailed" || Number(item?.status) >= 400)) .length; const workbenchEventSourceFailureCount = naturalNetwork .filter((item) => urlPath(item?.url) === "/v1/workbench/events" && (item?.type === "requestfailed" || Number(item?.status) >= 400)) .length; const significantRequestFailed = requestFailed.filter( (item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes), ); const webPerformanceDiagnostics = extractWebPerformanceRuntimeDiagnostics(naturalNetwork, promptTimes); const webPerformancePayloadStates = summarizeWebPerformancePayloadStates(naturalNetwork); const webPerformanceDiagnosticGroups = groupWebPerformanceRuntimeDiagnostics(webPerformanceDiagnostics); const domDiagnostics = []; const executionErrors = []; const baselineExecutionErrors = []; const canarySessionIds = sessionInvariantCanarySessionIds(control); const firstPromptMs = promptTimes.length > 0 ? promptTimes[0] : Infinity; const firstSeenExecutionErrorMs = new Map(); for (const sample of samples) { const tsMs = Date.parse(sample?.ts); const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; if (Array.isArray(sample?.diagnostics)) { for (const diagnostic of sample.diagnostics.slice(0, 12)) { const text = diagnostic?.textPreview || diagnostic?.text || ""; if (!String(text).trim()) continue; const parsedDiagnostic = parseDomDiagnosticSummary(text); domDiagnostics.push({ seq: sample.seq ?? null, ts: sample.ts ?? null, promptIndex, source: "diagnostic-node", className: diagnostic.className ?? null, diagnosticCode: diagnostic.diagnosticCode ?? parsedDiagnostic.diagnosticCode, traceId: diagnostic.traceId ?? parsedDiagnostic.traceId, httpStatus: diagnostic.httpStatus ?? parsedDiagnostic.httpStatus, idleSeconds: diagnostic.idleSeconds ?? parsedDiagnostic.idleSeconds, waitingFor: diagnostic.waitingFor ?? parsedDiagnostic.waitingFor, lastEventLabel: diagnostic.lastEventLabel ?? parsedDiagnostic.lastEventLabel, compact: diagnostic.compact ?? null, expanded: diagnostic.expanded ?? null, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null, textHash: diagnostic.textHash || sha256(text), preview: limitText(text, 260) }); } } const texts = sampleTexts(sample).filter(isDomDiagnosticSampleText); for (const text of texts.slice(0, 4)) { const parsedDiagnostic = parseDomDiagnosticSummary(text); domDiagnostics.push({ seq: sample.seq ?? null, ts: sample.ts ?? null, promptIndex, source: "sample-text", diagnosticCode: parsedDiagnostic.diagnosticCode, traceId: parsedDiagnostic.traceId, httpStatus: parsedDiagnostic.httpStatus, idleSeconds: parsedDiagnostic.idleSeconds, waitingFor: parsedDiagnostic.waitingFor, lastEventLabel: parsedDiagnostic.lastEventLabel, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null, textHash: sha256(text), preview: limitText(text, 220) }); } const seenExecutionErrors = new Set(); for (const candidate of sampleExecutionErrorCandidates(sample)) { const parsed = parseExecutionErrorText(candidate.text); if (!parsed) continue; const textHash = sha256(candidate.text); const dedupeKey = [candidate.source, candidate.traceId || "-", parsed.backend || "-", parsed.code || "-", parsed.status || "-", textHash].join("|"); if (seenExecutionErrors.has(dedupeKey)) continue; seenExecutionErrors.add(dedupeKey); const firstSeenMs = firstSeenExecutionErrorMs.has(dedupeKey) ? firstSeenExecutionErrorMs.get(dedupeKey) : tsMs; if (!firstSeenExecutionErrorMs.has(dedupeKey) && Number.isFinite(tsMs)) firstSeenExecutionErrorMs.set(dedupeKey, tsMs); const baseline = Number.isFinite(firstSeenMs) && firstSeenMs < firstPromptMs; const event = { seq: sample.seq ?? null, ts: sample.ts ?? null, promptIndex, baseline, firstSeenAt: Number.isFinite(firstSeenMs) ? new Date(firstSeenMs).toISOString() : null, 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, preview: limitText(candidate.text, 260) }; const eventSessions = [event.routeSessionId, event.activeSessionId].filter(Boolean); const nonCanarySession = canarySessionIds.size > 0 && !eventSessions.some((sessionId) => canarySessionIds.has(sessionId)); if (baseline || nonCanarySession) baselineExecutionErrors.push({ ...event, baseline: true, baselineReason: nonCanarySession ? "non-canary-session" : "pre-first-prompt" }); else executionErrors.push(event); 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, preview: limitText(candidate.text, 220) }); } } const consoleAlerts = consoleEvents .filter((item) => /error|warning|warn|assert/iu.test(String(item?.type || "")) || isDiagnosticText(item?.text)) .map((item) => consoleAlertEvent(item, promptTimes)); const significantConsoleAlerts = consoleAlerts.filter((item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes)); const pageErrors = errors.map((item) => ({ ts: item.ts ?? null, promptIndex: promptIndexForTs(promptTimes, item.ts), type: item.type ?? null, pageRole: item.pageRole ?? item.error?.details?.pageRole ?? null, pageId: item.pageId ?? item.error?.details?.pageId ?? null, routeSessionId: item.routeSessionId ?? item.error?.details?.routeSessionId ?? null, activeSessionId: item.activeSessionId ?? item.error?.details?.activeSessionId ?? null, commandId: item.commandId ?? item.error?.details?.commandId ?? null, sampleSeq: item.sampleSeq ?? item.error?.details?.sampleSeq ?? null, timeoutMs: timeoutMsFromMessage(item.error?.message || item.message || item.error || ""), errorName: item.error?.name ?? item.name ?? null, messageHash: item.error?.message ? sha256(item.error.message) : item.message ? sha256(item.message) : null, preview: limitText(item.error?.message || item.message || item.error || "", 220) })); return { summary: { httpErrorCount: httpErrors.length, requestFailedCount: requestFailed.length, significantRequestFailedCount: significantRequestFailed.length, workbenchSessionListReadCount, workbenchTraceEventsReadCount, webPerformanceBeaconFailureCount, webPerformancePayloadRequestCount: webPerformancePayloadStates.total, webPerformancePayloadParsedCount: webPerformancePayloadStates.parsed, webPerformancePayloadParseIssueCount: webPerformancePayloadStates.parseIssue, webPerformanceRuntimeDiagnosticCount: webPerformanceDiagnostics.length, webPerformanceRuntimeDiagnosticGroupCount: webPerformanceDiagnosticGroups.length, workbenchEventSourceFailureCount, benignLongLivedStreamClosureCount: requestFailed.length - significantRequestFailed.length, domDiagnosticSampleCount: domDiagnostics.length, domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length, executionErrorCount: executionErrors.length, baselineExecutionErrorCount: baselineExecutionErrors.length, consoleAlertCount: consoleAlerts.length, significantConsoleAlertCount: significantConsoleAlerts.length, pageErrorCount: pageErrors.length, networkErrorGroupCount: groupNetworkAlerts(httpErrors).length, requestFailedGroupCount: groupNetworkAlerts(requestFailed).length, significantRequestFailedGroupCount: groupNetworkAlerts(significantRequestFailed).length, executionErrorGroupCount: groupExecutionErrors(executionErrors).length, baselineExecutionErrorGroupCount: groupExecutionErrors(baselineExecutionErrors).length, consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length, significantConsoleAlertGroupCount: groupConsoleAlerts(significantConsoleAlerts).length }, networkHttpErrorsByPath: groupNetworkAlerts(httpErrors), networkRequestFailedByPath: groupNetworkAlerts(requestFailed), networkSignificantRequestFailedByPath: groupNetworkAlerts(significantRequestFailed), webPerformancePayloadStates, webPerformanceRuntimeDiagnostics: webPerformanceDiagnostics.slice(0, 120), webPerformanceRuntimeDiagnosticsByCode: webPerformanceDiagnosticGroups, domDiagnostics: domDiagnostics.slice(-80), domDiagnosticsByText: groupDomDiagnostics(domDiagnostics), domDiagnosticsByFingerprint: groupDomDiagnostics(domDiagnostics).slice(0, 80), runtimeExecutionErrors: executionErrors.slice(0, 120), runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors), baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80), baselineRuntimeExecutionErrorsByCode: groupExecutionErrors(baselineExecutionErrors), consoleAlerts: consoleAlerts.slice(0, 80), consoleAlertsByPath: groupConsoleAlerts(consoleAlerts), significantConsoleAlerts: significantConsoleAlerts.slice(0, 80), significantConsoleAlertsByPath: groupConsoleAlerts(significantConsoleAlerts), pageErrors: pageErrors.slice(0, 40) }; } function groupDomDiagnostics(events) { const groups = new Map(); for (const item of events || []) { const preview = String(item?.preview || "").trim(); if (!isReportableDomDiagnostic(item, preview)) continue; const normalizedPreview = normalizeDiagnosticPreview(preview); const key = [ item?.diagnosticCode || "", normalizedPreview ].join("|"); const existing = groups.get(key) || { source: item?.source || null, sources: new Set(), diagnosticCode: item?.diagnosticCode || null, textHash: item?.textHash || null, normalizedPreview, preview, count: 0, firstAt: item?.ts || null, lastAt: item?.ts || null, promptIndexes: new Set(), traceIds: new Set(), sampleSeqs: [] }; if (item?.source) existing.sources.add(String(item.source)); existing.count += 1; existing.firstAt = minIso(existing.firstAt, item?.ts || null); existing.lastAt = maxIso(existing.lastAt, item?.ts || null); if (Number.isFinite(Number(item?.promptIndex))) existing.promptIndexes.add(Number(item.promptIndex)); for (const traceId of extractDiagnosticTraceIds(item, preview)) existing.traceIds.add(traceId); if (existing.sampleSeqs.length < 12 && item?.seq !== undefined && item?.seq !== null) existing.sampleSeqs.push(item.seq); groups.set(key, existing); } return Array.from(groups.values()) .map((item) => ({ source: item.source, sources: Array.from(item.sources).sort(), diagnosticCode: item.diagnosticCode, textHash: item.textHash, normalizedPreview: item.normalizedPreview, preview: item.preview, count: item.count, firstAt: item.firstAt, lastAt: item.lastAt, promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b), traceIds: Array.from(item.traceIds).sort(), sampleSeqs: item.sampleSeqs })) .sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || ""))); } function extractWebPerformanceRuntimeDiagnostics(network, promptTimes) { const rows = []; for (const item of Array.isArray(network) ? network : []) { const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null; if (!payload || payload.parseStatus !== "parsed" || payload.schemaVersion !== "hwlab-web-performance-v2") continue; const events = Array.isArray(payload.events) ? payload.events : []; for (const event of events) { if (!event || typeof event !== "object") continue; const diagnosticCode = limitText(event.diagnosticCode || event.code || "", 120); const reason = limitText(event.reason || "", 120); const eventType = limitText(event.eventType || event.type || event.kind || "", 120); if (!diagnosticCode && !reason && !eventType) continue; rows.push({ ts: item.ts ?? event.ts ?? null, promptIndex: promptIndexForTs(promptTimes, item.ts ?? event.ts), pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, commandId: item.commandId ?? null, method: item.method ?? null, urlPath: urlPath(item.url), schemaVersion: payload.schemaVersion, captureStatus: payload.captureStatus ?? null, parseStatus: payload.parseStatus ?? null, byteCount: numberOrNull(payload.byteCount), bodyHash: payload.bodyHash ?? null, eventType, diagnosticCode, reason, module: limitText(event.module || "", 120), traceId: event.traceId ?? null, sessionIdHash: event.sessionIdHash ?? null, eventIdHash: event.eventIdHash ?? null, eventCount: numberOrNull(event.eventCount), deliveredCount: numberOrNull(event.deliveredCount), chunkCount: numberOrNull(event.chunkCount), flushDurationMs: numberOrNull(event.flushDurationMs), droppedCount: numberOrNull(event.droppedCount), maxItemsPerChunk: numberOrNull(event.maxItemsPerChunk), maxChunkMs: numberOrNull(event.maxChunkMs), replacedByKey: typeof event.replacedByKey === "boolean" || typeof event.replacedByKey === "number" ? event.replacedByKey : null, replacedByKeyHash: event.replacedByKeyHash ?? null, valuesRedacted: true, }); } } return rows.sort((left, right) => String(left.ts || "").localeCompare(String(right.ts || ""))).slice(-200); } function summarizeWebPerformancePayloadStates(network) { const stateCounts = new Map(); let total = 0; let parsed = 0; let parseIssue = 0; let overLimit = 0; let invalidJson = 0; let missingBody = 0; let unsupportedSchema = 0; let capturedEventCount = 0; let storedEventCount = 0; for (const item of Array.isArray(network) ? network : []) { const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null; if (!payload) continue; total += 1; const state = String(payload.parseStatus || payload.captureStatus || "unknown"); stateCounts.set(state, (stateCounts.get(state) || 0) + 1); if (payload.parseStatus === "parsed") parsed += 1; else parseIssue += 1; if (payload.parseStatus === "not-parsed-over-limit" || payload.captureStatus === "skipped-over-limit") overLimit += 1; if (payload.parseStatus === "invalid-json") invalidJson += 1; if (payload.parseStatus === "missing-body") missingBody += 1; if (payload.parseStatus === "unsupported-schema") unsupportedSchema += 1; if (Number.isFinite(Number(payload.eventCount))) capturedEventCount += Number(payload.eventCount); if (Number.isFinite(Number(payload.storedEventCount))) storedEventCount += Number(payload.storedEventCount); } return { total, parsed, parseIssue, overLimit, invalidJson, missingBody, unsupportedSchema, capturedEventCount, storedEventCount, states: Array.from(stateCounts.entries()).map(([state, count]) => ({ state, count })).sort((a, b) => b.count - a.count || a.state.localeCompare(b.state)), valuesRedacted: true, }; } function groupWebPerformanceRuntimeDiagnostics(events) { const groups = new Map(); for (const item of Array.isArray(events) ? events : []) { const key = [item.diagnosticCode || item.eventType || "-", item.reason || "-", item.module || "-"].join("|"); const group = groups.get(key) || { diagnosticCode: item.diagnosticCode || null, reason: item.reason || null, module: item.module || null, eventType: item.eventType || null, count: 0, firstAt: item.ts || null, lastAt: item.ts || null, promptIndexes: new Set(), traceIds: new Set(), eventCount: 0, deliveredCount: 0, chunkCount: 0, droppedCount: 0, maxFlushDurationMs: null, maxItemsPerChunk: null, maxChunkMs: null, replacedByKeyCount: 0, examples: [], }; group.count += 1; group.firstAt = minIso(group.firstAt, item.ts || null); group.lastAt = maxIso(group.lastAt, item.ts || null); if (Number.isFinite(Number(item.promptIndex))) group.promptIndexes.add(Number(item.promptIndex)); if (item.traceId) group.traceIds.add(String(item.traceId)); group.eventCount += Number(item.eventCount || 0); group.deliveredCount += Number(item.deliveredCount || 0); group.chunkCount += Number(item.chunkCount || 0); group.droppedCount += Number(item.droppedCount || 0); group.maxFlushDurationMs = maxRuntimeNumber(group.maxFlushDurationMs, item.flushDurationMs); group.maxItemsPerChunk = maxRuntimeNumber(group.maxItemsPerChunk, item.maxItemsPerChunk); group.maxChunkMs = maxRuntimeNumber(group.maxChunkMs, item.maxChunkMs); if (Number.isFinite(Number(item.replacedByKey))) group.replacedByKeyCount += Number(item.replacedByKey); else if (item.replacedByKey === true || item.replacedByKeyHash) group.replacedByKeyCount += 1; if (group.examples.length < 6) group.examples.push({ ts: item.ts || null, traceId: item.traceId || null, eventCount: item.eventCount ?? null, deliveredCount: item.deliveredCount ?? null, chunkCount: item.chunkCount ?? null, flushDurationMs: item.flushDurationMs ?? null, droppedCount: item.droppedCount ?? null, maxItemsPerChunk: item.maxItemsPerChunk ?? null, maxChunkMs: item.maxChunkMs ?? null, replacedByKey: item.replacedByKey ?? null, replacedByKeyHash: item.replacedByKeyHash ?? null, valuesRedacted: true, }); groups.set(key, group); } return Array.from(groups.values()).map((item) => ({ diagnosticCode: item.diagnosticCode, reason: item.reason, module: item.module, eventType: item.eventType, count: item.count, firstAt: item.firstAt, lastAt: item.lastAt, promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b), traceIds: Array.from(item.traceIds).sort().slice(0, 12), eventCount: item.eventCount, deliveredCount: item.deliveredCount, chunkCount: item.chunkCount, droppedCount: item.droppedCount, maxFlushDurationMs: item.maxFlushDurationMs, maxItemsPerChunk: item.maxItemsPerChunk, maxChunkMs: item.maxChunkMs, replacedByKeyCount: item.replacedByKeyCount, examples: item.examples, valuesRedacted: true, })).sort((left, right) => right.count - left.count || String(left.firstAt || "").localeCompare(String(right.firstAt || ""))); } function maxRuntimeNumber(current, candidate) { const value = Number(candidate); if (!Number.isFinite(value)) return current ?? null; const existing = Number(current); return Number.isFinite(existing) ? Math.max(existing, value) : value; } function isReportableDomDiagnostic(item, preview) { if (item?.source === "diagnostic-node" || item?.source === "execution-row") return true; return /trace_id=|HTTP\s+\d{3}\b|Failed to load resource|ERR_[A-Z_]+|provider-unavailable|AgentRun error|超过\s*\d+\s*ms\s*无新活动|代理暂时无法连接上游|Trace 更新超时|加载失败/iu.test(String(preview || "")); } function normalizeDiagnosticPreview(text) { return String(text || "") .replace(/trace_id=[A-Za-z0-9_-]+/gu, "trace_id=:traceId") .replace(/\btrc_[A-Za-z0-9_-]+\b/gu, "trc_:traceId") .replace(/\bses_[A-Za-z0-9_-]+\b/gu, "ses_:sessionId") .replace(/\brun_[A-Za-z0-9_-]+\b/gu, "run_:runId") .replace(/\bcmd_[A-Za-z0-9_-]+\b/gu, "cmd_:commandId") .replace(/[!!]+$/gu, "") .replace(/\s+/gu, " ") .trim(); } function extractDiagnosticTraceIds(item, preview) { const ids = new Set(); if (item?.traceId) ids.add(String(item.traceId)); const text = String(preview || ""); for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]); for (const match of text.matchAll(/trace_id=([A-Za-z0-9_-]+)/gu)) ids.add(match[1]); return ids; } function minIso(a, b) { if (!a) return b || null; if (!b) return a || null; return Date.parse(a) <= Date.parse(b) ? a : b; } function maxIso(a, b) { if (!a) return b || null; if (!b) return a || null; return Date.parse(a) >= Date.parse(b) ? a : b; } 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); const specific = candidates.filter((candidate) => { const parsed = parseExecutionErrorText(candidate.text); return parsed && parsed.code !== "error"; }); return specific.length > 0 ? specific : candidates; } function parseExecutionErrorText(text) { const value = String(text || ""); const agentRunCodeMatch = value.match(/\bagentrun:error:([A-Za-z0-9_.:-]+)/u); 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 agentRunCode = cleanExecutionCode(agentRunCodeMatch?.[1] || ""); const rawCode = agentRunCode ? "agentrun:error:" + agentRunCode : providerUnavailable ? "provider-unavailable" : "agentrun:error"; return { backend: agentRunText || agentRunCodeMatch ? "agentrun" : "unknown", status: normalizeExecutionStatus(statusMatch?.[1] || "error"), code: agentRunCode || (providerUnavailable ? "provider-unavailable" : "error"), rawCode, totalSeconds: totalMatch ? parseClockDurationSeconds(totalMatch[1]) : null, traceId: traceMatch?.[0] || null }; } function cleanExecutionCode(code) { const value = String(code || "").replace(/(?:AgentRun|Error|Failed).*$/u, "").replace(/[^A-Za-z0-9_.:-].*$/u, ""); return value || 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); const location = compactLocation(item.location); const traceMatch = (location?.urlPath || text).match(/\btrc_[A-Za-z0-9_-]+\b/u); return { ts: item.ts ?? null, promptIndex: promptIndexForTs(promptTimes, item.ts), type: item.type ?? null, status: statusMatch ? Number(statusMatch[1]) : null, urlPath: location?.urlPath || "-", traceId: traceMatch?.[0] || null, textHash: item.text ? sha256(item.text) : null, preview: limitText(text, 220), location }; } function groupConsoleAlerts(events) { const groups = new Map(); for (const event of events) { const key = [event.type || "-", event.status ?? "-", event.urlPath || "-"].join(" "); const group = groups.get(key) || { type: event.type ?? null, status: event.status ?? null, urlPath: event.urlPath || "-", count: 0, firstAt: event.ts, lastAt: event.ts, promptIndexes: [], traceIds: [] }; 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); groups.set(key, group); } return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); } function isBenignLongLivedStreamClosureAlert(event) { if (event?.urlPath !== "/v1/workbench/events") return false; if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false; const text = String(event.failureKind || event.errorPreview || event.preview || ""); return /ERR_NETWORK_CHANGED|ERR_ABORTED|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed/iu.test(text); } function isObserverRefreshClosureAlert(event, observerRefreshTimes) { const urlPath = String(event?.urlPath || ""); if (!["/v1/workbench/events", "/v1/web-performance"].includes(urlPath) && !/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath)) return false; if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false; const text = String(event.failureKind || event.errorPreview || event.preview || ""); if (!/ERR_NETWORK_CHANGED|ERR_ABORTED|ERR_INCOMPLETE_CHUNKED_ENCODING|ERR_INVALID_CHUNKED_ENCODING|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed|incomplete chunked|invalid chunked/iu.test(text)) return false; const ts = Date.parse(String(event.ts || "")); return Number.isFinite(ts) && observerRefreshTimes.some((refreshTs) => Math.abs(ts - refreshTs) <= 8000); } function networkAlertEvent(item, promptTimes) { const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null; return { ts: item.ts ?? null, promptIndex: promptIndexForTs(promptTimes, item.ts), method: String(item.method || "GET").toUpperCase(), status: Number.isFinite(Number(item.status)) ? Number(item.status) : null, type: item.type ?? null, urlPath: urlPath(item.url), urlHash: item.url ? sha256(item.url) : null, failureKind: failureText ? String(failureText) : null, errorTextHash: failureText ? sha256(failureText) : null, errorPreview: failureText ? limitText(failureText, 160) : null }; } function groupNetworkAlerts(events) { const groups = new Map(); for (const event of events) { const key = [event.method, event.urlPath, event.status ?? "-", event.type].join(" "); const group = groups.get(key) || { method: event.method, urlPath: event.urlPath, status: event.status, type: event.type, count: 0, firstAt: event.ts, lastAt: event.ts, promptIndexes: [], failureKinds: [], errorTextHashes: [] }; group.count += 1; group.lastAt = event.ts; if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); if (event.failureKind && !group.failureKinds.includes(event.failureKind)) group.failureKinds.push(event.failureKind); if (event.errorTextHash && !group.errorTextHashes.includes(event.errorTextHash)) group.errorTextHashes.push(event.errorTextHash); groups.set(key, group); } return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); } function isDiagnosticText(text) { const value = String(text || ""); return /Failed to (?:fetch|load resource)|request failed|net::ERR_[A-Z0-9_:-]+|server responded with a status of [45][0-9]{2}|HTTP\s+[45][0-9]{2}\b|trace_id=|workbench turn\s*超过|turn\s*超过|无新活动|idle\s+\d+s|waitingFor=|lastEventLabel=|无法连接上游|代理暂时无法连接上游|provider-unavailable|agentrun:error|AgentRun error|projection-resume|sync-failed|durable projection store|realtime-gap|Trace 更新超时|加载失败|请求失败|请求已失败/iu.test(value); } `; }