fix(web-probe): report runtime alerts
This commit is contained in:
@@ -471,8 +471,33 @@ 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, .message-diagnostic, .projection-diagnostic, [data-testid*="diagnostic" i], [class*="diagnostic" i], [role="alert"], .alert, .warning, .error';
|
||||
const messages = summarize(messageSelector, 20);
|
||||
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 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 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,
|
||||
tag: element.tagName.toLowerCase(),
|
||||
className: String(element.className || "").slice(0, 240),
|
||||
testId: element.getAttribute("data-testid"),
|
||||
role: element.getAttribute("role"),
|
||||
compact: element.getAttribute("data-compact"),
|
||||
expanded: element.getAttribute("data-expanded") || element.getAttribute("aria-expanded"),
|
||||
title: element.getAttribute("title"),
|
||||
diagnosticCode,
|
||||
traceId: traceMatch?.[1] || null,
|
||||
httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null,
|
||||
idleSeconds: idleMatch ? Number(idleMatch[1]) : null,
|
||||
text,
|
||||
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
|
||||
};
|
||||
});
|
||||
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);
|
||||
@@ -504,6 +529,7 @@ async function samplePage(reason) {
|
||||
scroll: { x: Math.round(window.scrollX), y: Math.round(window.scrollY), height: Math.round(document.documentElement.scrollHeight), width: Math.round(document.documentElement.scrollWidth) },
|
||||
messages,
|
||||
traceRows,
|
||||
diagnostics,
|
||||
turns,
|
||||
performance: performance.getEntriesByType("resource").slice(-30).map((entry) => ({ name: entry.name.split(/[?#]/u)[0].slice(0, 240), initiatorType: entry.initiatorType, startTime: Math.round(entry.startTime), duration: Math.round(entry.duration) })),
|
||||
};
|
||||
@@ -528,8 +554,9 @@ function digestDom(dom) {
|
||||
if (dom && dom.error) return dom;
|
||||
const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
||||
return { ...dom, messages, traceRows, turns };
|
||||
return { ...dom, messages, traceRows, diagnostics, turns };
|
||||
}
|
||||
|
||||
async function captureScreenshot(reason, imageType = "png") {
|
||||
@@ -678,6 +705,7 @@ const reportMdPath = path.join(analysisDir, "report.md");
|
||||
const samples = await readJsonl(path.join(stateDir, "samples.jsonl"));
|
||||
const control = await readJsonl(path.join(stateDir, "control.jsonl"));
|
||||
const network = await readJsonl(path.join(stateDir, "network.jsonl"));
|
||||
const consoleEvents = await readJsonl(path.join(stateDir, "console.jsonl"));
|
||||
const errors = await readJsonl(path.join(stateDir, "errors.jsonl"));
|
||||
const artifacts = await readJsonl(path.join(stateDir, "artifacts.jsonl"));
|
||||
const manifest = await readJson(path.join(stateDir, "manifest.json"));
|
||||
@@ -687,7 +715,8 @@ await mkdir(analysisDir, { recursive: true, mode: 0o700 });
|
||||
const transitions = buildTransitions(samples);
|
||||
const sampleMetrics = buildSampleMetrics(samples, control);
|
||||
const promptNetwork = buildPromptNetworkReport(control, network);
|
||||
const findings = buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork);
|
||||
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
|
||||
const findings = buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts);
|
||||
const commandTimeline = control.filter((item) => item.phase === "completed" || item.phase === "failed").map((item) => ({ ts: item.ts, phase: item.phase, commandId: item.commandId, type: item.type, input: item.input, afterUrl: item.afterUrl }));
|
||||
const report = {
|
||||
ok: findings.filter((item) => item.severity === "red").length === 0,
|
||||
@@ -696,11 +725,12 @@ const report = {
|
||||
stateDir,
|
||||
manifest: compactManifest(manifest),
|
||||
heartbeat: compactHeartbeat(heartbeat),
|
||||
counts: { samples: samples.length, control: control.length, network: network.length, errors: errors.length, artifacts: artifacts.length },
|
||||
counts: { samples: samples.length, control: control.length, network: network.length, console: consoleEvents.length, errors: errors.length, artifacts: artifacts.length },
|
||||
commandTimeline,
|
||||
transitions,
|
||||
sampleMetrics,
|
||||
promptNetwork,
|
||||
runtimeAlerts,
|
||||
findings,
|
||||
artifactSummary: await artifactSummary(artifacts),
|
||||
safety: { offlineOnly: true, browserDriven: false, apiFetch: false, valuesRedacted: true },
|
||||
@@ -708,7 +738,7 @@ const report = {
|
||||
await writeFile(reportJsonPath, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 });
|
||||
await writeFile(reportMdPath, renderMarkdown(report), { mode: 0o600 });
|
||||
const [jsonMeta, mdMeta] = await Promise.all([fileMeta(reportJsonPath), fileMeta(reportMdPath)]);
|
||||
console.log(JSON.stringify({ ok: true, command: "web-probe-observe analyze", stateDir, reportJsonPath, reportJsonSha256: jsonMeta.sha256, reportMdPath, reportMdSha256: mdMeta.sha256, counts: report.counts, sampleMetrics: sampleMetrics.summary, promptNetwork: promptNetwork.summary, findings: findings.slice(0, 20), valuesRedacted: true }, null, 2));
|
||||
console.log(JSON.stringify({ ok: true, command: "web-probe-observe analyze", stateDir, reportJsonPath, reportJsonSha256: jsonMeta.sha256, reportMdPath, reportMdSha256: mdMeta.sha256, counts: report.counts, sampleMetrics: sampleMetrics.summary, promptNetwork: promptNetwork.summary, runtimeAlerts: runtimeAlerts.summary, findings: findings.slice(0, 20), valuesRedacted: true }, null, 2));
|
||||
|
||||
async function readJson(file) {
|
||||
try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; }
|
||||
@@ -723,7 +753,7 @@ async function readJsonl(file) {
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork) {
|
||||
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts) {
|
||||
const findings = [];
|
||||
const commandTimes = control.filter((item) => item.phase === "completed" || item.phase === "started").map((item) => Date.parse(item.ts)).filter(Number.isFinite);
|
||||
const routeSessions = new Set(samples.map((item) => item.routeSessionId).filter(Boolean));
|
||||
@@ -753,6 +783,10 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
||||
if (traceSeen && !traceTerminal) findings.push({ id: "trace-without-terminal", severity: "amber", summary: "trace rows were visible but no terminal status was sampled", firstTraceSample: ref(samples.find((item) => Array.isArray(item.traceRows) && item.traceRows.length > 0)) });
|
||||
const promptFailures = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.chatPostOk === false) : [];
|
||||
if (promptFailures.length > 0) findings.push({ id: "prompt-chat-submit-failed", severity: "red", summary: "sendPrompt command had no successful /v1/agent/chat POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) });
|
||||
if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) });
|
||||
if ((runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.requestFailedCount, groups: runtimeAlerts.networkRequestFailedByPath.slice(0, 12) });
|
||||
if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
|
||||
if ((runtimeAlerts?.summary?.consoleAlertCount ?? 0) > 0) findings.push({ id: "runtime-console-alerts", severity: "amber", summary: "browser console warning/error entries were captured during observation", count: runtimeAlerts.summary.consoleAlertCount, groups: runtimeAlerts.consoleAlertsByPath.slice(0, 12) });
|
||||
const naturalApi = network.filter((item) => item.observerInitiated === false && item.type === "response" && /\/v1\/|\/auth\//u.test(String(item.url || "")));
|
||||
findings.push({ id: "natural-api-dom-lag-baseline", severity: "info", summary: "natural API responses and DOM samples are available for manual lag correlation", naturalApiResponses: naturalApi.length, sampleCount: samples.length });
|
||||
if (errors.length > 0) findings.push({ id: "browser-console-or-page-errors", severity: "amber", summary: "pageerror/runner errors were captured", count: errors.length, first: errors.slice(0, 5) });
|
||||
@@ -848,6 +882,172 @@ function buildPromptNetworkReport(control, network) {
|
||||
};
|
||||
}
|
||||
|
||||
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 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 domDiagnostics = [];
|
||||
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;
|
||||
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,
|
||||
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(isDiagnosticText);
|
||||
for (const text of texts.slice(0, 4)) {
|
||||
domDiagnostics.push({
|
||||
seq: sample.seq ?? null,
|
||||
ts: sample.ts ?? null,
|
||||
promptIndex,
|
||||
source: "sample-text",
|
||||
routeSessionId: sample.routeSessionId ?? null,
|
||||
activeSessionId: sample.activeSessionId ?? null,
|
||||
textHash: sha256(text),
|
||||
preview: limitText(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 pageErrors = errors.map((item) => ({
|
||||
ts: item.ts ?? null,
|
||||
promptIndex: promptIndexForTs(promptTimes, item.ts),
|
||||
type: item.type ?? null,
|
||||
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,
|
||||
domDiagnosticSampleCount: domDiagnostics.length,
|
||||
consoleAlertCount: consoleAlerts.length,
|
||||
pageErrorCount: pageErrors.length,
|
||||
networkErrorGroupCount: groupNetworkAlerts(httpErrors).length,
|
||||
requestFailedGroupCount: groupNetworkAlerts(requestFailed).length,
|
||||
consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length
|
||||
},
|
||||
networkHttpErrorsByPath: groupNetworkAlerts(httpErrors),
|
||||
networkRequestFailedByPath: groupNetworkAlerts(requestFailed),
|
||||
domDiagnostics: domDiagnostics.slice(0, 80),
|
||||
consoleAlerts: consoleAlerts.slice(0, 80),
|
||||
consoleAlertsByPath: groupConsoleAlerts(consoleAlerts),
|
||||
pageErrors: pageErrors.slice(0, 40)
|
||||
};
|
||||
}
|
||||
|
||||
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 networkAlertEvent(item, promptTimes) {
|
||||
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: item.failureKind ?? null,
|
||||
errorTextHash: item.errorText ? sha256(item.errorText) : 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: []
|
||||
};
|
||||
group.count += 1;
|
||||
group.lastAt = event.ts;
|
||||
if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
|
||||
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) {
|
||||
return /Failed to fetch|request failed|realtime-gap|durable projection store|turn 超过|无法连接|暂时|报警|告警|警告|失败|错误|异常|超时|timeout|error|failed|warning|502|503|504|provider-unavailable|projection-resume/iu.test(String(text || ""));
|
||||
}
|
||||
|
||||
function buildSampleMetrics(samples, control) {
|
||||
const promptCommands = control
|
||||
.filter((item) => item.type === "sendPrompt" && item.phase === "completed")
|
||||
@@ -1082,7 +1282,7 @@ function buildRoundMetricSummaries(timeline, promptCommands) {
|
||||
|
||||
function sampleTexts(sample) {
|
||||
const rows = [];
|
||||
for (const group of [sample?.messages, sample?.traceRows]) {
|
||||
for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) {
|
||||
if (!Array.isArray(group)) continue;
|
||||
for (const item of group) {
|
||||
const text = String(item?.textPreview || "");
|
||||
@@ -1136,6 +1336,11 @@ function latestPromptIndex(promptTimes, tsMs) {
|
||||
return index;
|
||||
}
|
||||
|
||||
function promptIndexForTs(promptTimes, ts) {
|
||||
const tsMs = Date.parse(ts);
|
||||
return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
|
||||
}
|
||||
|
||||
function buildTransitions(samples) {
|
||||
const rows = [];
|
||||
let last = null;
|
||||
@@ -1166,7 +1371,8 @@ function detectFinalFlicker(samples) {
|
||||
function digestSample(sample) {
|
||||
const messages = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textHash || item.textPreview || "").join("|") : "";
|
||||
const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => (item.status || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : "";
|
||||
return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace);
|
||||
const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => (item.className || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : "";
|
||||
return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics);
|
||||
}
|
||||
|
||||
function nearCommand(sample, commandTimes, windowMs) {
|
||||
@@ -1245,6 +1451,22 @@ function renderMarkdown(report) {
|
||||
const commandLines = report.commandTimeline.length === 0 ? "- 无控制命令。" : report.commandTimeline.map((item) => "- " + item.ts + " " + item.phase + " " + item.type + " " + item.commandId + " " + (item.afterUrl || "")).join("\n");
|
||||
const transitionLines = report.transitions.length === 0 ? "- 无状态变化。" : report.transitions.slice(0, 80).map((item) => "- #" + item.seq + " " + item.ts + " messages=" + item.messageCount + " traceRows=" + item.traceRowCount + " route=" + (item.routeSessionId || "-") + " active=" + (item.activeSessionId || "-")).join("\n");
|
||||
const metricSummary = report.sampleMetrics?.summary || {};
|
||||
const alertSummary = report.runtimeAlerts?.summary || {};
|
||||
const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0
|
||||
? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
|
||||
: "- 无 HTTP 错误。";
|
||||
const requestFailedLines = Array.isArray(report.runtimeAlerts?.networkRequestFailedByPath) && report.runtimeAlerts.networkRequestFailedByPath.length > 0
|
||||
? report.runtimeAlerts.networkRequestFailedByPath.slice(0, 40).map((item) => "- requestfailed " + item.method + " " + item.urlPath + " count=" + item.count + " 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")
|
||||
: "- 无 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")
|
||||
: "- 无 console warning/error。";
|
||||
const consoleAlertGroupLines = Array.isArray(report.runtimeAlerts?.consoleAlertsByPath) && report.runtimeAlerts.consoleAlertsByPath.length > 0
|
||||
? report.runtimeAlerts.consoleAlertsByPath.slice(0, 40).map((item) => "- console " + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " traces=" + (item.traceIds?.slice(0, 6).join(",") || "-")).join("\n")
|
||||
: "- 无 console 分组。";
|
||||
const promptNetworkLines = Array.isArray(report.promptNetwork?.rounds) && report.promptNetwork.rounds.length > 0
|
||||
? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n")
|
||||
: "- 无 prompt 网络记录。";
|
||||
@@ -1261,6 +1483,7 @@ function renderMarkdown(report) {
|
||||
+ "- samples: " + report.counts.samples + "\n"
|
||||
+ "- control: " + report.counts.control + "\n"
|
||||
+ "- network: " + report.counts.network + "\n"
|
||||
+ "- console: " + (report.counts.console ?? 0) + "\n"
|
||||
+ "- errors: " + report.counts.errors + "\n\n"
|
||||
+ "## Findings\n\n" + findingLines + "\n\n"
|
||||
+ "## Sample metrics\n\n"
|
||||
@@ -1271,6 +1494,17 @@ function renderMarkdown(report) {
|
||||
+ "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n"
|
||||
+ "### Rounds\n\n" + roundLines + "\n\n"
|
||||
+ "### Prompt network\n\n" + promptNetworkLines + "\n\n"
|
||||
+ "### Runtime alerts\n\n"
|
||||
+ "- httpErrorCount: " + (alertSummary.httpErrorCount ?? 0) + "\n"
|
||||
+ "- requestFailedCount: " + (alertSummary.requestFailedCount ?? 0) + "\n"
|
||||
+ "- domDiagnosticSampleCount: " + (alertSummary.domDiagnosticSampleCount ?? 0) + "\n"
|
||||
+ "- consoleAlertCount: " + (alertSummary.consoleAlertCount ?? 0) + "\n"
|
||||
+ "- pageErrorCount: " + (alertSummary.pageErrorCount ?? 0) + "\n\n"
|
||||
+ "#### HTTP errors\n\n" + httpAlertLines + "\n\n"
|
||||
+ "#### Request failed\n\n" + requestFailedLines + "\n\n"
|
||||
+ "#### DOM diagnostics\n\n" + domDiagnosticLines + "\n\n"
|
||||
+ "#### Console alerts\n\n" + consoleAlertLines + "\n\n"
|
||||
+ "#### Console alert groups\n\n" + consoleAlertGroupLines + "\n\n"
|
||||
+ "### Turn timing table\n\n"
|
||||
+ turnTimingTable + "\n\n"
|
||||
+ "### Aggregate timeline\n\n"
|
||||
@@ -1296,5 +1530,16 @@ function urlPath(value) {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
function compactLocation(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
return { urlPath: urlPath(value.url), lineNumber: value.lineNumber ?? null, columnNumber: value.columnNumber ?? null };
|
||||
}
|
||||
|
||||
function limitText(value, limit) {
|
||||
const text = String(value ?? "");
|
||||
if (text.length <= limit) return text;
|
||||
return text.slice(0, Math.max(0, limit - 1)) + "…";
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user