diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 790258b5..f6a61e5c 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -568,6 +568,38 @@ async function samplePage(reason) { return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none"; }; const textHashInput = (element) => trim(element.textContent || "", 800); + const diagnosticSummaryText = (element) => { + const summarySelectors = [ + ".api-error-diagnostic-summary-text p", + ".api-error-diagnostic-summary-text", + "[class*='diagnostic-summary-text' i] p", + "[class*='diagnostic-summary-text' i]", + "[data-testid*='diagnostic-summary' i]", + "[data-testid*='error-summary' i]", + "[role='alert'] p", + "[role='alert']" + ]; + const parts = []; + for (const selector of summarySelectors) { + for (const candidate of Array.from(element.querySelectorAll(selector))) { + if (!visible(candidate)) continue; + const text = trim(candidate.textContent || "", 800); + if (text && !parts.includes(text)) parts.push(text); + } + } + const ownText = textHashInput(element); + const text = parts.length > 0 ? parts.join(" ") : ownText; + return text.replace(/\s+(?:!|i诊断|诊断详情)$/u, "").trim(); + }; + const diagnosticToggleOnly = (element, text) => { + const compact = String(text || "").trim(); + if (!/^(?:!|i诊断|诊断|诊断详情)$/u.test(compact)) return false; + const tag = element.tagName.toLowerCase(); + const role = element.getAttribute("role"); + const aria = element.getAttribute("aria-label") || ""; + const title = element.getAttribute("title") || ""; + return tag === "button" || role === "button" || /诊断/u.test(aria) || /诊断/u.test(title); + }; const summarize = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => { const rect = element.getBoundingClientRect(); return { @@ -591,10 +623,13 @@ async function samplePage(reason) { const traceRows = summarize(traceSelector, 30); const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => { const rect = element.getBoundingClientRect(); - const text = textHashInput(element); + const text = diagnosticSummaryText(element); + if (!text || diagnosticToggleOnly(element, text)) return null; const traceMatch = text.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu); const httpStatusMatch = text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); const idleMatch = text.match(/\bidle\s+(\d+)s\b/iu); + const waitingForMatch = text.match(/\bwaitingFor=([^\s;;,,)]+)/iu); + const lastEventLabelMatch = text.match(/\blastEventLabel=([^\s;;,,)]+)/iu); const diagnosticCode = httpStatusMatch ? "http-" + httpStatusMatch[1] : /turn\s*超过|无新活动/iu.test(text) ? "turn-idle-no-activity" : /Failed to fetch/iu.test(text) ? "failed-to-fetch" : "diagnostic"; return { index, @@ -609,10 +644,12 @@ async function samplePage(reason) { traceId: traceMatch?.[1] || null, httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null, idleSeconds: idleMatch ? Number(idleMatch[1]) : null, + waitingFor: waitingForMatch?.[1] || null, + lastEventLabel: lastEventLabelMatch?.[1] || null, text, rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, }; - }); + }).filter(Boolean); const turns = Array.from(document.querySelectorAll('article.message-card[data-role="agent"], .message-card[data-role="agent"], article[data-role="agent"]')).filter(visible).map((element, index) => { const rect = element.getBoundingClientRect(); const text = textHashInput(element); @@ -1007,6 +1044,30 @@ function buildPromptNetworkReport(control, network) { }; } +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 buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { const promptTimes = control .filter((item) => item.type === "sendPrompt" && item.phase === "completed") @@ -1032,16 +1093,19 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { 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 ?? null, - traceId: diagnostic.traceId ?? null, - httpStatus: diagnostic.httpStatus ?? null, - idleSeconds: diagnostic.idleSeconds ?? 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, @@ -1053,11 +1117,18 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { } const texts = sampleTexts(sample).filter(isDiagnosticText); 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), @@ -1941,7 +2012,7 @@ function renderMarkdown(report) { ? report.runtimeAlerts.networkRequestFailedByPath.slice(0, 40).map((item) => "- requestfailed " + item.method + " " + item.urlPath + " count=" + item.count + " failure=" + (item.failureKinds?.slice(0, 4).join(",") || "-") + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n") : "- 无 requestfailed。"; const domDiagnosticLines = Array.isArray(report.runtimeAlerts?.domDiagnostics) && report.runtimeAlerts.domDiagnostics.length > 0 - ? report.runtimeAlerts.domDiagnostics.slice(0, 40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " source=" + (item.source || "-") + " code=" + (item.diagnosticCode || "-") + " traceId=" + (item.traceId || "-") + " http=" + (item.httpStatus ?? "-") + " idle=" + (item.idleSeconds ?? "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") + ? report.runtimeAlerts.domDiagnostics.slice(0, 40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " source=" + (item.source || "-") + " code=" + (item.diagnosticCode || "-") + " traceId=" + (item.traceId || "-") + " http=" + (item.httpStatus ?? "-") + " idle=" + (item.idleSeconds ?? "-") + " waitingFor=" + (item.waitingFor || "-") + " lastEventLabel=" + (item.lastEventLabel || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") : "- 无 DOM 诊断文本。"; const consoleAlertLines = Array.isArray(report.runtimeAlerts?.consoleAlerts) && report.runtimeAlerts.consoleAlerts.length > 0 ? report.runtimeAlerts.consoleAlerts.slice(0, 40).map((item) => "- " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " type=" + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " traceId=" + (item.traceId || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n")