Merge pull request #455 from pikasTech/fix/hwlab-450-trace-preview

修复 Code Agent trace 预览计数展示
This commit is contained in:
Lyon
2026-05-24 18:24:02 +08:00
committed by GitHub
5 changed files with 248 additions and 17 deletions
@@ -88,6 +88,9 @@ test("source/default workbench report cannot claim DEV-LIVE and documents the co
false
);
assert.equal(report.checks.find((check) => check.id === "code-agent-long-timeout-contract")?.status, "pass");
const traceDisclosure = report.checks.find((check) => check.id === "code-agent-trace-replay-disclosure");
assert.equal(traceDisclosure?.status, "pass");
assert.deepEqual(traceDisclosure?.evidence, ["显示最近 14 / 总计 N", "展开全部", "复制 JSON", "下载 trace", "internal scroll for full trace"]);
});
test("Code Agent browser failure classifier emits Chinese timeout/provider/browser categories with traceId", () => {
+101 -8
View File
@@ -618,6 +618,11 @@ function runStaticSmoke() {
evidence: ["DEFAULT_API_TIMEOUT_MS=4500 for light probes", `DEFAULT_CODE_AGENT_TIMEOUT_MS=${codeAgentLongTimeoutMs}`, "sendAgentMessage passes timeoutMs"]
});
addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels disclose tail preview counts and provide full UI/JSON/download access for long traces.", {
blocker: "observability_blocker",
evidence: ["显示最近 14 / 总计 N", "展开全部", "复制 JSON", "下载 trace", "internal scroll for full trace"]
});
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider/stdio blockers without exposing credential internals and only long-lived Codex stdio replies can become full Code Agent completion.", {
blocker: "observability_blocker",
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"]
@@ -2618,6 +2623,28 @@ function hasCodeAgentLongTimeoutContract(files) {
);
}
function hasCodeAgentTraceReplayDisclosure({ app, styles }) {
const tracePanelBody = functionBody(app, "messageTracePanel");
const traceToolbarBody = functionBody(app, "messageTraceToolbar");
return (
/CODE_AGENT_TRACE_PREVIEW_LIMIT\s*=\s*14/u.test(app) &&
/function\s+messageTraceCountText\s*\(/u.test(app) &&
/显示最近\s+\$\{CODE_AGENT_TRACE_PREVIEW_LIMIT\}\s+\/\s+总计\s+\$\{total\}/u.test(app) &&
/function\s+renderTraceEventList\s*\(/u.test(app) &&
/tracePreviewEvents\(events\)/u.test(tracePanelBody) &&
/messageTraceToolbar\(message,\s*trace,\s*events,\s*list\)/u.test(tracePanelBody) &&
/展开全部/u.test(traceToolbarBody) &&
/复制 JSON/u.test(traceToolbarBody) &&
/下载 trace/u.test(traceToolbarBody) &&
/function\s+messageTraceJson\s*\(/u.test(app) &&
/function\s+downloadTraceJson\s*\(/u.test(app) &&
/\.message-trace-toolbar\s*\{/u.test(styles) &&
/\.message-trace-count\s*\{/u.test(styles) &&
/\.message-trace-action\s*\{/u.test(styles) &&
/\.message-trace-events-full\s*\{[\s\S]*?max-height:\s*min\(360px,\s*42vh\)[\s\S]*?overflow:\s*auto/u.test(styles)
);
}
function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus }) {
return (
/id=["']code-agent-summary["']/u.test(html) &&
@@ -4289,6 +4316,23 @@ async function inspectJourneyUi(page) {
const runningActions = summarizeActions("status-running");
const failedActions = summarizeActions("status-failed");
const timeoutActions = summarizeActions("status-timeout");
const tracePanels = [...document.querySelectorAll(".message-card.message-agent .message-trace")];
const tracePanelMetrics = tracePanels.map((panel) => {
const countText = panel.querySelector(".message-trace-count")?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const actions = [...panel.querySelectorAll(".message-trace-action")].map((button) => button.textContent?.replace(/\s+/gu, " ").trim() ?? "");
const events = [...panel.querySelectorAll(".message-trace-events li")].map((item) => item.textContent?.replace(/\s+/gu, " ").trim() ?? "");
return {
countText,
actions,
eventCount: events.length,
traceMode: panel.querySelector(".message-trace-events")?.dataset.traceMode ?? "",
tailPreviewVisible: /显示最近\s+14\s*\/\s*总计\s+(?:1[5-9]|[2-9]\d+)/u.test(countText),
fullEntryVisible: actions.includes("展开全部"),
copyJsonVisible: actions.includes("复制 JSON"),
downloadTraceVisible: actions.includes("下载 trace"),
cappedTailCount: events.length <= 14
};
});
return {
title: document.title,
workspaceHidden: document.querySelector('[data-view="workspace"]')?.hidden ?? null,
@@ -4331,16 +4375,24 @@ async function inspectJourneyUi(page) {
[...document.querySelectorAll(".message-card.status-source .message-meta")]
.some((element) => /SOURCE|source-fixture|trc_/u.test(element.textContent ?? ""))
),
traceText: [...document.querySelectorAll(".message-card.message-agent .message-trace")]
traceText: tracePanels
.map((element) => element.textContent ?? "")
.join("\n"),
traceHasTraceId: [...document.querySelectorAll(".message-card.message-agent .message-trace")]
traceHasTraceId: tracePanels
.some((element) => /trc_/u.test(element.textContent ?? "")),
traceShowsEvents: [...document.querySelectorAll(".message-card.message-agent .message-trace-events li")]
.some((element) => /#\d+|session|prompt|tool|assistant|timeout|blocked/u.test(element.textContent ?? "")),
tracePanelMetrics,
traceLongPreviewDisclosureVisible: tracePanelMetrics.some((metric) => metric.tailPreviewVisible),
traceFullViewEntryVisible: tracePanelMetrics.some((metric) => metric.fullEntryVisible),
traceCopyJsonVisible: tracePanelMetrics.some((metric) => metric.copyJsonVisible),
traceDownloadVisible: tracePanelMetrics.some((metric) => metric.downloadTraceVisible),
tracePreviewTailCapped: tracePanelMetrics
.filter((metric) => metric.tailPreviewVisible)
.every((metric) => metric.cappedTailCount && metric.traceMode === "tail"),
noMainAttributionNoise: document.querySelectorAll(".message-card.message-agent .message-attribution, .message-card.message-agent .message-evidence, .message-card.message-agent .code-agent-facts").length === 0,
attributionFallbackNotRunnerControl: !/openai-responses-fallback[\s\S]{0,80}(真实 runner|Codex stdio 长会话|workspace-write)/u.test(
[...document.querySelectorAll(".message-card.message-agent .message-trace")]
tracePanels
.map((element) => element.textContent ?? "")
.join("\n")
),
@@ -4831,6 +4883,11 @@ async function runLocalAgentFixtureSmoke({
ui.sourceMessageHasNonSensitiveMeta &&
ui.traceHasTraceId &&
ui.traceShowsEvents &&
ui.traceLongPreviewDisclosureVisible &&
ui.tracePreviewTailCapped &&
ui.traceFullViewEntryVisible &&
ui.traceCopyJsonVisible &&
ui.traceDownloadVisible &&
ui.noMainAttributionNoise &&
ui.attributionFallbackNotRunnerControl &&
ui.conversationFactsVisible &&
@@ -5547,6 +5604,44 @@ async function inspectQuickPromptGeometry(page, viewport) {
}, viewport);
}
function longSourceFixtureTraceEvents(traceId, count = 18) {
const labels = [
"source-fixture:session",
"prompt:queued",
"prompt:sent",
"context:loaded",
"session:reused",
"assistant:started",
"tool:skills.inspect",
"tool:skills.inspect:completed",
"tool:workspace.read",
"tool:workspace.read:completed",
"assistant:delta",
"assistant:delta",
"assistant:delta",
"assistant:delta",
"assistant:finalizing",
"message:stored",
"conversation:facts-updated",
"assistant:completed"
];
return Array.from({ length: count }, (_, index) => {
const seq = index + 1;
const label = labels[index] ?? `assistant:event:${seq}`;
const toolName = label.startsWith("tool:") ? label.split(":").slice(1).join(":") : undefined;
return {
seq,
traceId,
type: label.split(":")[0] || "event",
stage: label.split(":")[1] || "observed",
status: seq === count ? "completed" : "observed",
label,
...(toolName ? { toolName } : {}),
elapsedMs: seq * 37
};
});
}
async function startStaticWebServer(options = {}) {
const rootDir = path.resolve(options.rootDir ?? webRoot);
const authFixtureSessions = new Set();
@@ -6609,6 +6704,7 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
items: [],
blockers: []
};
const fixtureTraceEvents = longSourceFixtureTraceEvents(traceId);
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
@@ -6637,11 +6733,8 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
runnerTrace: {
traceId,
runnerKind: "openai-responses-fallback",
events: [
{ seq: 1, traceId, type: "session", stage: "fixture", status: "completed", label: "source-fixture:session" },
{ seq: 2, traceId, type: "assistant", stage: "completed", status: "completed", label: "assistant:completed" }
],
lastEvent: { seq: 2, traceId, type: "assistant", stage: "completed", status: "completed", label: "assistant:completed" }
events: fixtureTraceEvents,
lastEvent: fixtureTraceEvents.at(-1)
},
conversationFacts: {
conversationId,
+86 -9
View File
@@ -22,6 +22,7 @@ const TRACE_STREAM_FALLBACK_MS = 2500;
const TRACE_POLL_INTERVAL_MS = 1000;
const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 });
const CODE_AGENT_TIMEOUT_MS = resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 50, max: 300000 });
const CODE_AGENT_TRACE_PREVIEW_LIMIT = 14;
const rpcReadMethods = Object.freeze([
"system.health",
"cloud.adapter.describe",
@@ -3491,20 +3492,96 @@ function messageTracePanel(message) {
item.textContent = `trace=${message.traceId ?? "pending"};等待后端事件。`;
list.append(item);
} else {
for (const event of events.slice(-14)) {
const item = document.createElement("li");
item.append(
textSpan(`#${event.seq ?? "?"}`, "message-trace-seq"),
textSpan(event.label ?? `${event.type}:${event.status}`, "message-trace-label"),
textSpan(traceEventMeta(event), "message-trace-meta")
);
list.append(item);
}
const toolbar = messageTraceToolbar(message, trace, events, list);
list.dataset.traceMode = events.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? "tail" : "all";
renderTraceEventList(list, tracePreviewEvents(events));
details.append(summary, toolbar, list);
return details;
}
details.append(summary, list);
return details;
}
function messageTraceToolbar(message, trace, events, list) {
const toolbar = document.createElement("div");
toolbar.className = "message-trace-toolbar";
const count = textSpan(messageTraceCountText(events.length, false), "message-trace-count");
toolbar.append(count);
if (events.length > CODE_AGENT_TRACE_PREVIEW_LIMIT) {
let expanded = false;
const toggle = traceActionButton("展开全部", () => {
expanded = !expanded;
renderTraceEventList(list, expanded ? events : tracePreviewEvents(events));
list.classList.toggle("message-trace-events-full", expanded);
list.dataset.traceMode = expanded ? "all" : "tail";
count.textContent = messageTraceCountText(events.length, expanded);
toggle.textContent = expanded ? "收起为最近 14" : "展开全部";
}, "在面板内查看完整 trace 时间线");
toolbar.append(toggle);
}
toolbar.append(traceActionButton("复制 JSON", () => copyTextToClipboard(messageTraceJson(message, trace, events)), "复制完整 trace JSON"));
toolbar.append(traceActionButton("下载 trace", () => downloadTraceJson(message, trace, events), "下载完整 trace JSON 文件"));
return toolbar;
}
function tracePreviewEvents(events) {
return events.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? events.slice(-CODE_AGENT_TRACE_PREVIEW_LIMIT) : events;
}
function messageTraceCountText(total, expanded) {
if (total <= CODE_AGENT_TRACE_PREVIEW_LIMIT || expanded) return `显示全部 ${total} / 总计 ${total}`;
return `显示最近 ${CODE_AGENT_TRACE_PREVIEW_LIMIT} / 总计 ${total}`;
}
function renderTraceEventList(list, events) {
list.replaceChildren();
for (const event of events) {
const item = document.createElement("li");
item.append(
textSpan(`#${event.seq ?? "?"}`, "message-trace-seq"),
textSpan(event.label ?? `${event.type}:${event.status}`, "message-trace-label"),
textSpan(traceEventMeta(event), "message-trace-meta")
);
list.append(item);
}
}
function traceActionButton(label, onClick, title) {
const button = document.createElement("button");
button.type = "button";
button.className = "message-trace-action";
button.textContent = label;
button.title = title;
button.addEventListener("click", onClick);
return button;
}
function messageTraceJson(message, trace, events) {
return JSON.stringify({
traceId: trace?.traceId ?? message.traceId ?? null,
messageId: message.id ?? null,
eventCount: events.length,
lastEvent: trace?.lastEvent ?? events.at(-1) ?? null,
events
}, null, 2);
}
function downloadTraceJson(message, trace, events) {
const traceId = String(trace?.traceId ?? message.traceId ?? "trace")
.replace(/[^A-Za-z0-9_.-]+/gu, "-")
.replace(/^-|-$/gu, "")
.slice(0, 96) || "trace";
const blob = new Blob([messageTraceJson(message, trace, events)], { type: "application/json" });
const href = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = href;
anchor.download = `${traceId}.json`;
document.body.append(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(href), 0);
}
function runnerTraceHeadline(message, trace) {
const count = Array.isArray(trace?.events) ? trace.events.length : 0;
const last = trace?.lastEvent?.label ?? trace?.eventLabels?.at?.(-1) ?? "等待事件";
+13
View File
@@ -774,6 +774,19 @@ assert.match(app, /codexStdioFeasibility:\s*result\.codexStdioFeasibility/);
assert.match(app, /longLivedSessionGate:\s*result\.longLivedSessionGate/);
assert.match(app, /conversationFacts:\s*result\.conversationFacts/);
assert.match(app, /function messageTracePanel/);
assert.match(app, /CODE_AGENT_TRACE_PREVIEW_LIMIT\s*=\s*14/);
assert.match(app, /function messageTraceToolbar/);
assert.match(app, /function messageTraceCountText/);
assert.match(app, /显示最近\s+\$\{CODE_AGENT_TRACE_PREVIEW_LIMIT\}\s+\/\s+总计\s+\$\{total\}/);
assert.match(app, /展开全部/);
assert.match(app, /复制 JSON/);
assert.match(app, /下载 trace/);
assert.match(app, /function renderTraceEventList/);
assert.match(app, /function downloadTraceJson/);
assert.match(styles, /\.message-trace-toolbar\s*\{/);
assert.match(styles, /\.message-trace-count\s*\{/);
assert.match(styles, /\.message-trace-action\s*\{/);
assert.match(styles, /\.message-trace-events-full\s*\{[\s\S]*?max-height:\s*min\(360px,\s*42vh\)[\s\S]*?overflow:\s*auto/);
assert.match(app, /function runnerTraceHeadline/);
assert.match(app, /function traceEventMeta/);
assert.match(app, /function subscribeRunnerTrace/);
+45
View File
@@ -1258,6 +1258,44 @@ h3 {
list-style-position: inside;
}
.message-trace-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin: 7px 0 0;
}
.message-trace-count {
min-height: 24px;
display: inline-flex;
align-items: center;
padding: 2px 7px;
border: 1px solid var(--line-soft);
background: rgba(20, 24, 23, 0.68);
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
}
.message-trace-action {
min-height: 24px;
padding: 2px 7px;
border: 1px solid var(--line-strong);
background: var(--surface-2);
color: var(--muted);
cursor: pointer;
font-family: var(--mono);
font-size: 10px;
}
.message-trace-action:hover,
.message-trace-action:focus-visible {
border-color: var(--accent);
color: var(--text);
outline: 0;
}
.message-trace-events {
display: grid;
gap: 5px;
@@ -1268,6 +1306,13 @@ h3 {
font-size: 10px;
}
.message-trace-events-full {
max-height: min(360px, 42vh);
overflow: auto;
overscroll-behavior: contain;
padding-right: 4px;
}
.message-trace-events li {
min-width: 0;
overflow-wrap: anywhere;