Merge pull request #455 from pikasTech/fix/hwlab-450-trace-preview
修复 Code Agent trace 预览计数展示
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) ?? "等待事件";
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user