Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-analyzer-request-runtime-source.ts
T

1100 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
`;
}