diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 7ec6e634..989c274c 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -7444,7 +7444,102 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec "node -e \"const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(fs.readFileSync(process.argv[2], 'utf8').replace(/\\s+/g, ''), 'base64'))\" \"$analyzer\" \"$analyzer_b64\"", "rm -f \"$analyzer_b64\"", "chmod 700 \"$analyzer\"", - "UNIDESK_WEB_OBSERVE_STATE_DIR=\"$state_dir\" node \"$analyzer\"", + "mkdir -p \"$state_dir/analysis\"", + "analysis_stdout=\"$state_dir/analysis/analyzer-stdout.json\"", + "analysis_stderr=\"$state_dir/analysis/analyzer-stderr.log\"", + "set +e", + "UNIDESK_WEB_OBSERVE_STATE_DIR=\"$state_dir\" node \"$analyzer\" >\"$analysis_stdout\" 2>\"$analysis_stderr\"", + "analyzer_exit=$?", + "set -e", + "report_json=\"$state_dir/analysis/report.json\"", + "report_md=\"$state_dir/analysis/report.md\"", + "node - \"$analysis_stdout\" \"$analysis_stderr\" \"$report_json\" \"$report_md\" \"$analyzer_exit\" <<'UNIDESK_WEB_OBSERVE_ANALYZE_COMPACT'", + "const fs = require('fs');", + "const crypto = require('crypto');", + "const [stdoutPath, stderrPath, reportJsonPath, reportMdPath, analyzerExitRaw] = process.argv.slice(2);", + "const analyzerExit = Number(analyzerExitRaw);", + "const readText = (path) => { try { return fs.readFileSync(path, 'utf8'); } catch { return ''; } };", + "const readJson = (path) => { const text = readText(path); if (!text.trim()) return null; try { return JSON.parse(text); } catch { return null; } };", + "const sha256 = (path) => { const text = readText(path); return text ? 'sha256:' + crypto.createHash('sha256').update(text).digest('hex') : null; };", + "const statSize = (path) => { try { return fs.statSync(path).size; } catch { return 0; } };", + "const tail = (text, limit = 1200) => String(text || '').slice(-limit);", + "const takeHead = (value, limit) => Array.isArray(value) ? value.slice(0, limit) : [];", + "const takeTail = (value, limit) => Array.isArray(value) ? value.slice(-limit) : [];", + "const clip = (value, limit = 160) => value === null || value === undefined ? null : String(value).slice(0, limit);", + "const stdoutJson = readJson(stdoutPath);", + "const reportJson = readJson(reportJsonPath);", + "const source = (stdoutJson && stdoutJson.ok !== false ? stdoutJson : null) || reportJson || stdoutJson || null;", + "const objectOrNull = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;", + "const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, sampleCount: v.sampleCount ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, promptTextHash: clip(v.promptTextHash, 80) }; };", + "const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };", + "const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null }; };", + "const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.code, 48), code: clip(v.code ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };", + "const slimDomGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180) }; };", + "const slimDomSample = (item) => { const v = objectOrNull(item) || {}; return { seq: v.seq ?? null, ts: v.ts ?? null, source: clip(v.source, 32), diagnosticCode: clip(v.diagnosticCode, 48), traceId: clip(v.traceId, 64), httpStatus: v.httpStatus ?? null, idleSeconds: v.idleSeconds ?? null, waitingFor: clip(v.waitingFor, 48), lastEventLabel: clip(v.lastEventLabel, 80), text: clip(v.text ?? v.preview, 180) }; };", + "const slimConsoleGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path, 96), lastAt: v.lastAt ?? v.firstAt ?? null, firstAt: v.firstAt ?? null, traceIds: Array.isArray(v.traceIds) ? v.traceIds.slice(0, 3).map((x) => clip(x, 64)) : [] }; };", + "const slimConsoleSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path, 96), traceId: clip(v.traceId, 64), text: clip(v.text, 180) }; };", + "const slimJump = (item) => { const v = objectOrNull(item) || {}; return { columnLabel: v.columnLabel ?? null, promptIndex: v.promptIndex ?? null, fromSeq: v.fromSeq ?? null, toSeq: v.toSeq ?? null, fromValue: v.fromValue ?? null, toValue: v.toValue ?? null, delta: v.delta ?? null, sampleDeltaSeconds: v.sampleDeltaSeconds ?? null, allowedIncreaseSeconds: v.allowedIncreaseSeconds ?? null, traceId: v.traceId ?? null }; };", + "const srcMetrics = objectOrNull(source?.sampleMetrics);", + "const metrics = srcMetrics ? {", + " roundItems: takeTail(srcMetrics.roundItems, 8).map(slimRound),", + " rounds: Array.isArray(srcMetrics.rounds) ? takeTail(srcMetrics.rounds, 8).map(slimRound) : srcMetrics.rounds,", + " turnColumns: takeTail(srcMetrics.turnColumns, 8).map(slimTurnColumn),", + " turnTimingRows: srcMetrics.turnTimingRows ?? null,", + " turnTimingNonMonotonicCount: srcMetrics.turnTimingNonMonotonicCount ?? null,", + " turnTimingTotalElapsedDecreaseCount: srcMetrics.turnTimingTotalElapsedDecreaseCount ?? null,", + " turnTimingTerminalElapsedGrowthCount: srcMetrics.turnTimingTerminalElapsedGrowthCount ?? null,", + " turnTimingRecentUpdateJumpCount: srcMetrics.turnTimingRecentUpdateJumpCount ?? null,", + " turnTimingRecentUpdateSawtoothJumpCount: srcMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? null,", + " turnTimingRecentUpdateMaxIncreaseSeconds: srcMetrics.turnTimingRecentUpdateMaxIncreaseSeconds ?? null,", + " promptSegments: srcMetrics.promptSegments ?? null", + "} : null;", + "const srcRuntimeAlerts = objectOrNull(source?.runtimeAlerts);", + "const runtimeAlerts = srcRuntimeAlerts ? { httpErrorCount: srcRuntimeAlerts.httpErrorCount ?? null, requestFailedCount: srcRuntimeAlerts.requestFailedCount ?? null, domDiagnosticSampleCount: srcRuntimeAlerts.domDiagnosticSampleCount ?? null, consoleAlertCount: srcRuntimeAlerts.consoleAlertCount ?? null } : null;", + "const srcPagePerformance = objectOrNull(source?.pagePerformance);", + "const pagePerformance = srcPagePerformance ? { sameOriginApiPaths: srcPagePerformance.sameOriginApiPaths ?? null, slowPathCount: srcPagePerformance.slowPathCount ?? null, slowSampleCount: srcPagePerformance.slowSampleCount ?? null, worstP95Ms: srcPagePerformance.worstP95Ms ?? null } : null;", + "const srcPromptNetwork = objectOrNull(source?.promptNetwork);", + "const promptNetwork = srcPromptNetwork ? { promptSegments: srcPromptNetwork.promptSegments ?? null } : null;", + "const compact = source ? {", + " ok: analyzerExit === 0 && source.ok !== false,", + " counts: source.counts ?? null,", + " sampleMetrics: metrics,", + " runtimeAlerts,", + " pagePerformance,", + " promptNetwork,", + " pagePerformanceSlowApi: takeHead(source.pagePerformanceSlowApi, 5).map(slimSlowApi),", + " findings: takeHead(source.findings, 8).map(slimFinding),", + " domDiagnosticGroups: takeHead(source.domDiagnosticGroups, 5).map(slimDomGroup),", + " domDiagnosticSamples: takeHead(source.domDiagnosticSamples, 5).map(slimDomSample),", + " consoleAlertGroups: takeHead(source.consoleAlertGroups, 5).map(slimConsoleGroup),", + " consoleAlertSamples: takeHead(source.consoleAlertSamples, 5).map(slimConsoleSample),", + " turnTimingRecentUpdateJumps: takeHead(source.turnTimingRecentUpdateJumps, 5).map(slimJump),", + " reportJsonPath: source.reportJsonPath || reportJsonPath,", + " reportJsonSha256: source.reportJsonSha256 || sha256(reportJsonPath),", + " reportMdPath: source.reportMdPath || reportMdPath,", + " reportMdSha256: source.reportMdSha256 || sha256(reportMdPath),", + " analyzer: {", + " exitCode: Number.isFinite(analyzerExit) ? analyzerExit : null,", + " recoveredFrom: stdoutJson && stdoutJson.ok !== false ? 'stdout' : reportJson ? 'report-file' : stdoutJson ? 'stdout-error' : 'missing-output',", + " stdoutBytes: statSize(stdoutPath),", + " stderrBytes: statSize(stderrPath),", + " stderrTail: tail(readText(stderrPath))", + " }", + "} : {", + " ok: false,", + " error: 'web-probe-analyzer-output-missing',", + " reportJsonPath,", + " reportMdPath,", + " analyzer: {", + " exitCode: Number.isFinite(analyzerExit) ? analyzerExit : null,", + " recoveredFrom: 'missing-output',", + " stdoutBytes: statSize(stdoutPath),", + " stderrBytes: statSize(stderrPath),", + " stderrTail: tail(readText(stderrPath))", + " }", + "};", + "console.log(JSON.stringify(compact));", + "UNIDESK_WEB_OBSERVE_ANALYZE_COMPACT", + "exit \"$analyzer_exit\"", ].join("\n"); const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds); const analysis = parseJsonObject(result.stdout); @@ -7457,7 +7552,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec lane: options.lane, workspace: spec.workspace, analysis, - result: compactCommandResult(result), + result: analysis === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result), valuesRedacted: true, }); } @@ -7473,6 +7568,7 @@ function withWebObserveAnalyzeRendered(value: Record): Rendered function renderWebObserveAnalyzeTable(value: Record): string { const analysis = record(value.analysis); + const analyzer = record(analysis?.analyzer); const result = record(value.result); const counts = record(analysis?.counts); const sampleMetrics = record(analysis?.sampleMetrics); @@ -7544,11 +7640,17 @@ function renderWebObserveAnalyzeTable(value: Record): string { "", ...(value.ok === false ? [ "Blocked detail:", - webObserveTable(["EXIT", "TIMEOUT", "STDOUT", "STDERR"], [[ + webObserveTable(["EXIT", "TIMEOUT", "ANALYZER", "STDOUT", "STDERR"], [[ webObserveText(result.exitCode), webObserveText(result.timedOut), + webObserveShort([ + analyzer?.recoveredFrom ? `source=${webObserveText(analyzer.recoveredFrom)}` : "", + analyzer?.exitCode !== undefined && analyzer?.exitCode !== null ? `exit=${webObserveText(analyzer.exitCode)}` : "", + analyzer?.stdoutBytes !== undefined && analyzer?.stdoutBytes !== null ? `stdout=${webObserveText(analyzer.stdoutBytes)}B` : "", + analyzer?.stderrBytes !== undefined && analyzer?.stderrBytes !== null ? `stderr=${webObserveText(analyzer.stderrBytes)}B` : "", + ].filter((part) => part !== "").join(" "), 120), webObserveShort(webObserveText(result.stdoutTail), 120), - webObserveShort(webObserveText(result.stderr), 120), + webObserveShort(webObserveText(analyzer?.stderrTail ?? result.stderr), 500), ]]), "", ] : []), diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 669d73e1..4e089097 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -813,7 +813,7 @@ async function samplePage(reason) { const activeSessionId = activeSession ? activeSession.getAttribute("data-session-id") : null; const messageSelector = '[data-testid*="message" i], [class*="message" i], article, [role="article"]'; const traceSelector = '[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]'; - const diagnosticSelector = '.api-error-diagnostic, .api-error-diagnostic.message-diagnostic, .api-error-diagnostic.projection-diagnostic, [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [role="alert"], .alert, .warning, .error'; + const diagnosticSelector = '.api-error-diagnostic, [class*="api-error-diagnostic" i], [class*="message-diagnostic" i], [class*="projection-diagnostic" i], [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [data-testid*="diagnostic" i], [role="alert"], [aria-live="assertive"]'; const messages = summarize(messageSelector, 20); const traceRows = summarize(traceSelector, 30); const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => { @@ -1720,6 +1720,28 @@ function parseDomDiagnosticSummary(text) { }; } +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") @@ -1767,7 +1789,7 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { }); } } - const texts = sampleTexts(sample).filter(isDiagnosticText); + const texts = sampleTexts(sample).filter(isDomDiagnosticSampleText); for (const text of texts.slice(0, 4)) { const parsedDiagnostic = parseDomDiagnosticSummary(text); domDiagnostics.push({