Files
pikasTech-HWLAB/web/hwlab-cloud-web/app-trace.ts
T
2026-05-31 11:39:18 +08:00

1208 lines
48 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function installWorkbenchTestHooks() {
if (!isLocalWorkbenchTestHost() || !new URLSearchParams(window.location.search).has("hwlab-test-hooks")) return;
window.__hwlabWorkbenchTestHooks = {
seedCompletedAgentMessage,
seedTraceMessage,
appendTraceEvents,
reconcileTraceResultFixture,
latestAgentMessageText,
traceScrollMetrics,
traceDomIdentity,
traceBodyDomIdentity,
traceRowTexts,
setTraceBodyScrollTop,
replaceTraceAssistantStream,
setTraceScrollTop,
setConversationScrollTop,
resetProgrammaticScrollWriteCount
};
}
function isLocalWorkbenchTestHost() {
return ["127.0.0.1", "localhost", "::1", "[::1]"].includes(window.location.hostname);
}
function seedCompletedAgentMessage(options = {}) {
state.chatMessages = [{
id: options.messageId ?? "msg_markdown_contract",
role: "agent",
title: options.title ?? "Code Agent 回复",
text: String(options.text ?? ""),
status: options.status ?? "completed",
traceId: options.traceId ?? "trc_markdown_contract",
conversationId: options.conversationId ?? "cnv_markdown_contract",
sessionId: options.sessionId ?? "ses_markdown_contract"
}];
renderConversation();
}
function seedTraceMessage(options = {}) {
const traceId = options.traceId ?? "trc_scroll_contract";
const messageId = options.messageId ?? "msg_scroll_contract";
const count = Math.max(1, Number(options.count ?? 80));
const events = Array.isArray(options.events)
? options.events.map((event, index) => normalizeTestTraceEvent(traceId, event, index + 1))
: Array.from({ length: count }, (_, index) => testTraceEvent(traceId, index + 1));
state.chatMessages = [{
id: messageId,
role: "agent",
title: "Code Agent 处理中",
text: "Trace scroll contract fixture",
status: "running",
traceId,
runnerTrace: {
traceId,
events,
assistantStreams: Array.isArray(options.assistantStreams) ? options.assistantStreams : [],
eventCount: Number.isInteger(options.eventCount) ? options.eventCount : events.length,
eventsCompacted: options.eventsCompacted === true,
eventWindow: options.eventWindow ?? null,
fullTraceLoaded: options.fullTraceLoaded === true,
lastEvent: options.lastEvent ?? events.at(-1),
waitingFor: "turn/completed"
}
}];
renderConversation();
}
function normalizeTestTraceEvent(traceId, event, seq) {
return {
traceId,
seq,
createdAt: new Date(1779690000000 + seq * 1000).toISOString(),
...event,
traceId: event?.traceId ?? traceId,
seq: event?.seq ?? seq,
createdAt: event?.createdAt ?? new Date(1779690000000 + seq * 1000).toISOString()
};
}
function appendTraceEvents(count = 1) {
const message = state.chatMessages.find((item) => item.runnerTrace?.traceId);
if (!message) return;
const trace = message.runnerTrace;
const start = Array.isArray(trace.events) ? trace.events.length : 0;
const additions = Array.from({ length: Math.max(1, Number(count)) }, (_, index) => testTraceEvent(trace.traceId, start + index + 1));
trace.events = [...trace.events, ...additions];
trace.eventCount = trace.events.length;
trace.lastEvent = trace.events.at(-1);
patchMessageTracePanel(message);
}
function replaceTraceAssistantStream(stream = {}) {
const message = state.chatMessages.find((item) => item.runnerTrace?.traceId);
if (!message) return null;
const trace = message.runnerTrace;
trace.assistantStreams = [{
itemId: "assistant-message",
chunkCount: Math.max(1, Number(stream.chunkCount ?? 1)),
createdAt: stream.createdAt ?? new Date(1779699000000).toISOString(),
updatedAt: stream.updatedAt ?? new Date(1779699001000 + Math.max(1, Number(stream.chunkCount ?? 1)) * 1000).toISOString(),
waitingFor: stream.waitingFor ?? "turn/completed",
text: String(stream.text ?? "assistant stream fixture")
}];
patchMessageTracePanel(message);
return traceBodyDomIdentity();
}
async function reconcileTraceResultFixture(result) {
const message = state.chatMessages.find((item) => item.runnerTrace?.traceId || item.traceId);
if (!message) return null;
applyCodeAgentResultToMessage(message.id, {
...(result && typeof result === "object" ? result : {}),
traceId: message.traceId,
conversationId: message.conversationId ?? "cnv_fixture",
sessionId: message.sessionId ?? "ses_fixture",
status: "completed",
provider: "codex-stdio",
model: "gpt-test",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
capabilityLevel: "long-lived-codex-stdio-session",
runner: {
kind: "codex-app-server-stdio-runner",
codexStdio: true,
writeCapable: true,
durableSession: true,
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
capabilityLevel: "long-lived-codex-stdio-session"
},
session: {
sessionId: message.sessionId ?? "ses_fixture",
status: "idle",
idleTimeoutMs: 1800000,
lastTraceId: message.traceId,
longLivedSession: true,
durableSession: true,
codexStdio: true,
writeCapable: true
},
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
sessionReuse: { reused: true, sessionId: message.sessionId ?? "ses_fixture" },
longLivedSessionGate: { status: "pass", pass: true },
toolCalls: [{ name: "codex-app-server.thread/resume+turn/start", status: "completed" }],
skills: [],
reply: {
content: "fixture reconciled result",
...(result?.reply && typeof result.reply === "object" ? result.reply : {})
},
providerTrace: {
protocol: "codex-app-server-jsonrpc-stdio",
command: "codex app-server --listen stdio://",
terminalStatus: "completed",
...(result?.providerTrace && typeof result.providerTrace === "object" ? result.providerTrace : {})
},
runnerTrace: message.runnerTrace,
updatedAt: new Date().toISOString()
}, {
baseMessage: message,
retryInput: message.retryInput,
retryOf: message.retryOf
});
renderCodeAgentSummary();
renderConversation();
return latestAgentMessageText();
}
function latestAgentMessageText() {
const message = [...state.chatMessages].reverse().find((item) => item.role === "agent");
return message ? {
title: message.title,
text: message.text,
status: message.status,
traceId: message.traceId,
providerTrace: message.providerTrace ?? null,
sourceKind: message.sourceKind ?? null,
capabilityLevel: message.capabilityLevel ?? null,
error: message.error ?? null
} : null;
}
function traceRowTexts() {
return [...el.conversationList.querySelectorAll(".message-trace-events[data-trace-ui-key] .message-trace-row")]
.map((row, index) => ({
index,
rowId: row.dataset.traceRowId ?? null,
text: row.textContent ?? ""
}));
}
function testTraceEvent(traceId, seq) {
return {
traceId,
label: "trace:scroll-contract",
type: "trace",
status: "observed",
createdAt: new Date(1779690000000 + seq * 1000).toISOString(),
message: `trace scroll contract event ${seq}\n${"payload ".repeat(18)}`,
waitingFor: "turn/completed"
};
}
function traceScrollMetrics() {
const conversation = el.conversationList;
const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]");
return {
conversationTop: conversation.scrollTop,
conversationBottomGap: scrollBottomGap(conversation),
traceTop: list?.scrollTop ?? null,
traceBottomGap: list ? scrollBottomGap(list) : null,
traceScrollHeight: list?.scrollHeight ?? null,
traceClientHeight: list?.clientHeight ?? null,
traceRowCount: list?.querySelectorAll(".message-trace-row").length ?? 0
};
}
function traceDomIdentity() {
const conversation = el.conversationList;
const panel = conversation.querySelector(".message-trace[data-trace-ui-key]");
const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]");
const firstRow = list?.querySelector(".message-trace-row") ?? null;
if (panel && !panel.__hwlabTraceIdentity) panel.__hwlabTraceIdentity = `panel-${Date.now()}-${Math.random()}`;
if (list && !list.__hwlabTraceIdentity) list.__hwlabTraceIdentity = `list-${Date.now()}-${Math.random()}`;
if (firstRow && !firstRow.__hwlabTraceIdentity) firstRow.__hwlabTraceIdentity = `row-${Date.now()}-${Math.random()}`;
return {
panel: panel?.__hwlabTraceIdentity ?? null,
list: list?.__hwlabTraceIdentity ?? null,
firstRow: firstRow?.__hwlabTraceIdentity ?? null,
rowCount: list?.querySelectorAll(".message-trace-row").length ?? 0,
programmaticScrollWrites: state.traceProgrammaticScrollWrites
};
}
function traceBodyDomIdentity(selector = ".message-trace-events[data-trace-ui-key] li:last-child .message-trace-body") {
const body = el.conversationList.querySelector(selector);
const row = body?.closest(".message-trace-row") ?? null;
if (body && !body.__hwlabTraceIdentity) body.__hwlabTraceIdentity = `body-${Date.now()}-${Math.random()}`;
if (row && !row.__hwlabTraceIdentity) row.__hwlabTraceIdentity = `row-${Date.now()}-${Math.random()}`;
return {
selector,
body: body?.__hwlabTraceIdentity ?? null,
row: row?.__hwlabTraceIdentity ?? null,
rowId: row?.dataset.traceRowId ?? null,
top: body?.scrollTop ?? null,
left: body?.scrollLeft ?? null,
scrollHeight: body?.scrollHeight ?? null,
clientHeight: body?.clientHeight ?? null,
text: body?.textContent ?? null
};
}
function resetProgrammaticScrollWriteCount() {
state.traceProgrammaticScrollWrites = 0;
return state.traceProgrammaticScrollWrites;
}
function setTraceScrollTop(top, { user = true } = {}) {
const list = el.conversationList.querySelector(".message-trace-events[data-trace-ui-key]");
if (!list) return traceScrollMetrics();
if (user) markTraceScrollIntent(list.dataset.traceUiKey);
list.scrollTop = top;
rememberTraceScrollPosition(list.dataset.traceUiKey, list);
return traceScrollMetrics();
}
function setTraceBodyScrollTop(selector, top, { user = true } = {}) {
const body = el.conversationList.querySelector(selector);
if (!body) return traceBodyDomIdentity(selector);
if (user) markTraceBodyScrollIntent(body);
body.scrollTop = top;
rememberTraceBodyScrollPosition(body);
return traceBodyDomIdentity(selector);
}
function setConversationScrollTop(top, { user = true } = {}) {
if (user) markConversationScrollIntent();
el.conversationList.scrollTop = top;
captureConversationScrollPosition();
return traceScrollMetrics();
}
export function traceDisplayRows(trace, events) {
const rows = [];
const commandGroups = traceCommandExecutionGroups(events);
const requestEvent = tracePrimaryRequestEvent(events);
const hasTerminalAssistant = events.some((event) => isTerminalAssistantTraceEvent(event));
const renderedCommands = new Set();
let requestRendered = false;
let setupRendered = false;
let completionRendered = false;
let assistantIndex = 0;
for (const event of events) {
if (!event) continue;
if (isToolOutputChunkTraceEvent(event)) continue;
if (isCommandExecutionTraceEvent(event)) {
const groupKey = traceCommandGroupKey(event);
const group = commandGroups.get(groupKey) ?? { key: groupKey, started: event, completed: event, outputs: [] };
const renderHere = event.label === "item/commandExecution:started" || group.started === event;
if (renderHere && !renderedCommands.has(groupKey)) {
const row = traceCommandSummaryRow(trace, group);
if (row) rows.push(row);
renderedCommands.add(groupKey);
}
continue;
}
if (isAssistantMessageTraceEvent(event)) {
const row = traceAssistantSummaryRow(trace, event, assistantIndex);
assistantIndex += 1;
if (row) rows.push(row);
continue;
}
if (isRequestTraceEvent(event)) {
if (!requestRendered) {
const row = traceRequestSummaryRow(trace, event, requestEvent ?? event);
if (row) rows.push(row);
requestRendered = true;
}
continue;
}
if (isSetupTraceEvent(event)) {
if (!setupRendered) {
const row = traceSetupSummaryRow(trace, events, event);
if (row) rows.push(row);
setupRendered = true;
}
continue;
}
if (isCompletionTraceEvent(event)) {
if (!completionRendered && !hasTerminalAssistant) {
const row = traceCompletionSummaryRow(trace, event);
if (row) rows.push(row);
completionRendered = true;
}
continue;
}
if (isNoisyTraceEvent(event)) continue;
const row = traceDisplayRow(trace, event);
if (row) rows.push(row);
}
return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean);
}
function compactTraceTextTail(text, maxLength) {
const clean = cleanTraceText(text);
if (clean.length <= maxLength) return clean;
return `...${clean.slice(-maxLength)}`;
}
function traceCommandExecutionGroups(events) {
const groups = new Map();
let activeKey = null;
const ensureGroup = (key, event) => {
const groupKey = key || traceCommandGroupKey(event);
if (!groups.has(groupKey)) groups.set(groupKey, { key: groupKey, started: null, completed: null, outputs: [] });
return groups.get(groupKey);
};
for (const event of events) {
if (isCommandExecutionTraceEvent(event)) {
const key = traceCommandGroupKey(event, activeKey);
const group = ensureGroup(key, event);
if (event.label === "item/commandExecution:started") {
group.started = group.started ?? event;
activeKey = key;
} else {
group.completed = event;
group.started = group.started ?? event;
if (activeKey === key) activeKey = null;
}
continue;
}
if (isToolOutputChunkTraceEvent(event)) {
const key = traceCommandGroupKey(event, activeKey);
ensureGroup(key, event).outputs.push(event);
}
}
return groups;
}
function traceCommandGroupKey(event, fallback = null) {
return traceNonEmptyString(event?.itemId ?? event?.callId ?? event?.commandId ?? event?.id) || fallback || `cmd:${event?.seq ?? event?.createdAt ?? Math.random()}`;
}
function traceCommandSummaryRow(trace, group) {
const start = group.started ?? group.completed ?? group.outputs?.[0];
const completed = group.completed ?? null;
if (!start) return null;
const command = displayTraceCommand(completed?.command ?? start.command ?? "");
const clock = traceClock(start.createdAt);
const title = traceCommandTitle(command);
const ok = !completed ? false : traceStatusToken(completed) === "ok" && (!Number.isInteger(completed.exitCode) || completed.exitCode === 0);
return {
rowId: `tool-summary:${group.key}`,
seq: start.seq ?? null,
tone: completed ? (ok ? "ok" : "blocked") : "warn",
header: `${clock} 🔧 工具调用:${title}`,
body: traceCommandSummaryBody(group, command)
};
}
function traceCommandSummaryBody(group, command) {
const completed = group.completed ?? null;
const result = traceCommandResultSummary(group, command);
return [
command ? `命令:${command}` : null,
completed ? `结果:${result}` : "结果:等待命令完成"
].filter(Boolean).join("\n");
}
function traceCommandResultSummary(group, command) {
const completed = group.completed ?? {};
const output = traceCommandCombinedOutput(group);
const fields = traceOutputFields(output);
const success = fields.hostSuccess ?? fields.success ?? commandSuccessFromEvent(completed);
const duration = traceCommandDurationSuffix(group);
if (fields.content && /read-after-launch-flash/u.test(command)) {
return `\n${traceStartupLogSummary(fields.content)}${duration}`;
}
if (fields.hostJobId) {
return [`hostJobId=${fields.hostJobId}`, `success=${success}`].join(", ") + duration;
}
if (fields.logTail && /Flash Load finished/iu.test(fields.logTail)) {
const ticks = fields.content ? traceTickRange(fields.content) : null;
const capture = ticks ? ` 捕获的部分串口:tick ${ticks}` : "";
return `success=${success}, Flash Load finished.${capture}${duration}`;
}
if (fields.logTail) {
return `success=${success}, logTail: "${traceLogTailSummary(fields.logTail)}"${duration}`;
}
if (fields.content) {
const ticks = traceTickRange(fields.content);
const content = ticks ? `捕获的串口:tick ${ticks}` : compactTraceOneLine(fields.content, 180);
return `success=${success}, ${content}${duration}`;
}
const compact = compactTraceOneLine(output || completed.errorMessage || completed.message || "", 220);
return `${success !== null ? `success=${success}` : traceStatusToken(completed)}${compact ? `, ${compact}` : ""}${duration}`;
}
function traceCommandCombinedOutput(group) {
return [
...(Array.isArray(group.outputs) ? group.outputs.map((event) => rawTraceOutputText(event)) : []),
group.completed?.stdoutSummary,
group.completed?.outputSummary,
group.completed?.stderrSummary
].filter(Boolean).join("\n");
}
function commandSuccessFromEvent(event) {
if (!event) return null;
if (Number.isInteger(event.exitCode)) return event.exitCode === 0;
const status = String(event.status ?? "");
if (["completed", "succeeded", "success"].includes(status)) return true;
if (["failed", "error", "timeout", "canceled"].includes(status)) return false;
return null;
}
function traceCommandDurationSuffix(group) {
const completed = group.completed ?? null;
if (!completed) return "";
let seconds = null;
if (typeof completed.durationMs === "number") {
seconds = completed.durationMs / 1000;
} else if (group.started?.createdAt && completed.createdAt) {
const start = Date.parse(group.started.createdAt);
const end = Date.parse(completed.createdAt);
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) seconds = (end - start) / 1000;
}
return seconds === null ? "" : ` (耗时 ${seconds.toFixed(1)}s)`;
}
function traceCommandTitle(command) {
if (/build\s+start/iu.test(command)) return "启动编译";
if (/build\s+status/iu.test(command)) return "等待编译并获取状态";
if (/download\s+start/iu.test(command)) return "启动下载(含串口捕获)";
if (/download\s+status/iu.test(command)) return "等待下载并获取状态";
if (/read-after-launch-flash/iu.test(command)) return "读取完整启动日志";
return "命令执行";
}
function displayTraceCommand(command) {
let text = cleanTraceOutputText(command);
text = text.replace(/^\/?\s*bin\s*\/\s*bash\s+-lc\s+(["'])([\s\S]*)\1$/u, "$2");
text = text.replace(/^bash\s+-lc\s+(["'])([\s\S]*)\1$/u, "$2");
return normalizeTraceCommandSpacing(text);
}
function normalizeTraceCommandSpacing(text) {
return String(text ?? "")
.replace(/:\s+\/\s+/gu, ":/")
.replace(/\s+\/\s+/gu, "/")
.replace(/\s{2,}/gu, " ")
.trim();
}
function traceOutputFields(text) {
return {
hostAction: traceOutputStringField(text, "hostAction"),
hostJobId: traceOutputStringField(text, "hostJobId"),
hostSuccess: traceOutputBooleanField(text, "hostSuccess"),
success: traceOutputBooleanField(text, "success"),
logTail: traceOutputStringField(text, "logTail"),
content: traceOutputStringField(text, "content")
};
}
function traceOutputStringField(text, name) {
const pattern = new RegExp(`"${name}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`, "gu");
let value = "";
for (const match of String(text ?? "").matchAll(pattern)) {
try {
const parsed = JSON.parse(`"${match[1]}"`);
if (parsed) value = parsed;
} catch {
if (match[1]) value = match[1];
}
}
return value;
}
function traceOutputBooleanField(text, name) {
const pattern = new RegExp(`"${name}"\\s*:\\s*(true|false)`, "giu");
let value = null;
for (const match of String(text ?? "").matchAll(pattern)) {
value = match[1].toLowerCase() === "true";
}
return value;
}
function traceLogTailSummary(value) {
const text = cleanSerialText(value);
const build = text.match(/([^\n]*\b\d+ Error\(s\), \d+ Warning\(s\)\.[^\n]*)(?:\n([^\n]*Build Time Elapsed:[^\n]*))?/iu);
if (build) return compactTraceOneLine(`... ${[build[1], build[2]].filter(Boolean).join(" ")}`, 220);
const flash = text.match(/Flash Load finished[^\n]*/iu);
if (flash) return flash[0];
return compactTraceOneLine(text, 220);
}
function traceStartupLogSummary(value) {
const lines = cleanSerialText(value)
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const selected = [];
for (const line of lines) {
if (/hello device-pod!/iu.test(line)) selected.push("hello device-pod!");
else if (/STM32F103 USART1 baseline ready/iu.test(line)) selected.push("[01_baseline] STM32F103 USART1 baseline ready");
else if (/baud=/iu.test(line)) selected.push(line.replace(/\s+\/\s+/gu, "/"));
else if (/project=/iu.test(line)) selected.push(line.replace(/\s+\/\s+/gu, "/"));
else if (/dap=.*idcode=/iu.test(line)) selected.push(line);
}
const ticks = traceTickRange(value);
if (ticks) {
const [first, last] = ticks.split("-");
selected.push(last ? `tick=${first}, ..., tick=${last}` : `tick=${first}`);
}
return selected.slice(0, 8).join("\n");
}
function traceTickRange(value) {
const ticks = [...String(value ?? "").matchAll(/tick=(\d+)/giu)].map((match) => Number(match[1])).filter(Number.isFinite);
if (ticks.length === 0) return null;
const first = ticks[0];
const last = ticks.at(-1);
return first === last ? String(first) : `${first}-${last}`;
}
function cleanSerialText(value) {
return String(value ?? "")
.replace(/\u0000/gu, "")
.replace(/\r\n|\r/gu, "\n")
.replace(/\n{3,}/gu, "\n\n")
.trim();
}
function compactTraceOneLine(value, limit = 220) {
const text = cleanSerialText(value).replace(/\s+/gu, " ").trim();
return text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 3))}...`;
}
function tracePrimaryRequestEvent(events) {
return events.find((event) => isRequestTraceEvent(event) && tracePromptText(event)) ?? events.find(isRequestTraceEvent) ?? null;
}
function traceRequestSummaryRow(trace, event, promptEvent = event) {
const clock = traceClock(event.createdAt);
const prompt = tracePromptText(promptEvent);
return {
rowId: `trace-request:${event.seq ?? "accepted"}`,
seq: event.seq ?? null,
tone: "warn",
header: `${clock} 请求接受${prompt ? `,提示词:"${compactTraceOneLine(prompt, 120)}"` : ""}`,
body: null
};
}
function traceSetupSummaryRow(trace, events, event) {
const setupEvents = events.filter(isSetupTraceEvent);
const sessionReused = setupEvents.some((item) => /session:reused/iu.test(String(item?.label ?? "")));
const promptSent = setupEvents.some((item) => /prompt:sent|sent prompt/iu.test(String(item?.label ?? "")));
const turnStarted = setupEvents.some((item) => /turn[:/]start|turn:started|turn\/start/iu.test(String(item?.label ?? "")));
const parts = [
sessionReused ? "会话复用" : "会话就绪",
promptSent ? "提示词已发送" : null,
turnStarted ? "轮次开始" : null
].filter(Boolean);
return {
rowId: "trace-setup:session-turn",
seq: event.seq ?? null,
tone: "source",
header: `${traceClock(event.createdAt)} ${parts.join("") || "会话准备完成"}`,
body: null
};
}
function traceCompletionSummaryRow(trace, event) {
return {
rowId: `trace-completion:${event.seq ?? "turn"}`,
seq: event.seq ?? null,
tone: traceEventTone(event),
header: `${traceClock(event.createdAt)} 轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, event))}`,
body: null
};
}
function traceAssistantSummaryRow(trace, event, index = 0) {
const terminal = isTerminalAssistantTraceEvent(event);
const text = traceAssistantTextForEvent(trace, event, index);
if (!text && !terminal && traceHasLaterTerminalAssistant(trace, event)) return null;
const elapsed = formatTraceDuration(traceRelativeMs(trace, event));
return {
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "assistant"}:${event.createdAt ?? "unknown"}`}`,
seq: event.seq ?? null,
tone: traceEventTone(event),
header: terminal
? `${traceClock(event.createdAt)} 🤖 助手最后一条消息,轮次完成(总耗时 ${elapsed}`
: `${traceClock(event.createdAt)} 🤖 助手消息${index === 0 ? "(第一条)" : ""}`,
body: text ? compactTraceAssistantMarkdown(text, terminal ? 5000 : 2200) : null,
bodyFormat: "markdown"
};
}
function traceAssistantTextForEvent(trace, event, index = 0) {
const direct = normalizeTraceAssistantMarkdown(event.message ?? event.outputSummary ?? event.chunk ?? event.text ?? "");
if (direct) return traceAssistantTextDuplicatesTerminal(trace, event, direct) ? "" : direct;
const streams = traceAssistantStreams(trace);
if (streams.length === 0) return "";
const itemId = traceNonEmptyString(event.itemId ?? event.messageId ?? event.id);
if (itemId) {
const match = streams.find((stream) => stream.itemId === itemId || stream.messageId === itemId || stream.id === itemId);
if (match?.text) {
const text = normalizeTraceAssistantMarkdown(match.text);
return traceAssistantTextDuplicatesTerminal(trace, event, text) ? "" : text;
}
}
if (isTerminalAssistantTraceEvent(event)) return traceAssistantTerminalText(trace, event);
return traceAssistantCumulativeSegment(trace, event, index);
}
function traceAssistantStreams(trace) {
return Array.isArray(trace?.assistantStreams)
? trace.assistantStreams.filter((stream) => stream && typeof stream === "object" && normalizeTraceAssistantMarkdown(stream.text ?? ""))
: [];
}
function traceAssistantEvents(trace) {
return Array.isArray(trace?.events) ? trace.events.filter(isAssistantMessageTraceEvent) : [];
}
function traceAssistantTerminalText(trace, event = null) {
const streams = traceAssistantStreams(trace);
const itemId = traceNonEmptyString(event?.itemId ?? event?.messageId ?? event?.id);
if (itemId) {
const matched = streams.find((stream) => stream.itemId === itemId || stream.messageId === itemId || stream.id === itemId);
if (matched?.text) return normalizeTraceAssistantMarkdown(matched.text);
}
return normalizeTraceAssistantMarkdown(streams.at(-1)?.text ?? "");
}
function traceAssistantTextDuplicatesTerminal(trace, event, text) {
if (isTerminalAssistantTraceEvent(event) || !traceHasLaterTerminalAssistant(trace, event)) return false;
const terminalText = traceAssistantTerminalText(trace);
return Boolean(terminalText && traceAssistantComparableText(text) === traceAssistantComparableText(terminalText));
}
function traceHasLaterTerminalAssistant(trace, event) {
const events = traceAssistantEvents(trace);
const currentIndex = events.findIndex((item) => item === event || (item?.seq !== undefined && item.seq === event?.seq));
if (currentIndex < 0) return events.some(isTerminalAssistantTraceEvent);
return events.slice(currentIndex + 1).some(isTerminalAssistantTraceEvent);
}
function traceAssistantCumulativeSegment(trace, event, index = 0) {
const assistantEvents = traceAssistantEvents(trace).filter((item) => !isTerminalAssistantTraceEvent(item));
const eventIndex = assistantEvents.findIndex((item) => item === event || (item?.seq !== undefined && item.seq === event?.seq));
const segmentIndex = eventIndex >= 0 ? eventIndex : index;
if (segmentIndex < 0) return "";
const stream = traceAssistantCumulativeStream(trace);
if (!stream?.text) return "";
const cumulative = normalizeTraceAssistantMarkdown(stream.text);
const terminalText = traceAssistantTerminalText(trace);
const nonTerminalText = traceAssistantRemoveTerminalSuffix(cumulative, terminalText);
if (!nonTerminalText || traceAssistantComparableText(nonTerminalText) === traceAssistantComparableText(terminalText)) return "";
const segments = traceAssistantSplitCumulativeText(nonTerminalText, assistantEvents.length);
return normalizeTraceAssistantMarkdown(segments[segmentIndex] ?? "");
}
function traceAssistantCumulativeStream(trace) {
const streams = traceAssistantStreams(trace);
if (streams.length === 0) return null;
const terminalIds = new Set(traceAssistantEvents(trace)
.filter(isTerminalAssistantTraceEvent)
.flatMap((event) => [event.itemId, event.messageId, event.id].map(traceNonEmptyString).filter(Boolean)));
const candidates = streams.filter((stream) => {
const streamId = traceNonEmptyString(stream.itemId ?? stream.messageId ?? stream.id);
return !streamId || !terminalIds.has(streamId);
});
return [...(candidates.length > 0 ? candidates : streams)]
.sort((left, right) => String(right.text ?? "").length - String(left.text ?? "").length)[0] ?? null;
}
function traceAssistantRemoveTerminalSuffix(text, terminalText) {
const clean = normalizeTraceAssistantMarkdown(text);
const terminal = normalizeTraceAssistantMarkdown(terminalText);
if (!clean || !terminal) return clean;
if (clean.endsWith(terminal)) return clean.slice(0, -terminal.length).trim();
const index = traceAssistantCompactLastIndex(clean, terminal);
if (index > 0) return clean.slice(0, index).trim();
return clean;
}
function traceAssistantCompactLastIndex(text, needle) {
const haystack = traceAssistantComparableWithMap(text);
const target = traceAssistantComparableText(needle);
if (!target) return -1;
const compactIndex = haystack.text.lastIndexOf(target);
return compactIndex >= 0 ? haystack.map[compactIndex] ?? -1 : -1;
}
function traceAssistantComparableWithMap(value) {
const map = [];
let text = "";
const raw = String(value ?? "");
for (let index = 0; index < raw.length; index += 1) {
const char = raw[index];
if (/\s/u.test(char)) continue;
text += char.toLowerCase();
map.push(index);
}
return { text, map };
}
function traceAssistantComparableText(value) {
return traceAssistantComparableWithMap(normalizeTraceAssistantMarkdown(value)).text;
}
function traceAssistantSplitCumulativeText(text, count) {
const targetCount = Math.max(1, Number(count) || 1);
const clean = normalizeTraceAssistantMarkdown(text);
if (!clean) return [];
if (targetCount === 1) return [clean];
const colonPieces = [];
let remaining = clean;
while (remaining && colonPieces.length < targetCount) {
const match = /^([\s\S]*?[:])(?=\s*[^\s])/u.exec(remaining);
if (!match?.[1] || match[1].trim().length < 8) break;
colonPieces.push(match[1].trim());
remaining = remaining.slice(match[1].length).trim();
}
const pieces = colonPieces.length > 0 ? colonPieces : traceAssistantSentencePieces(clean);
if (colonPieces.length > 0 && remaining) pieces.push(remaining);
if (pieces.length <= targetCount) return [...pieces, ...Array.from({ length: targetCount - pieces.length }, () => "")];
return traceAssistantPackPieces(pieces, targetCount);
}
function traceAssistantSentencePieces(text) {
return String(text ?? "").match(/[^。!?.!?]+[。!?.!?]?/gu)?.map((item) => item.trim()).filter(Boolean) ?? [];
}
function traceAssistantPackPieces(pieces, count) {
const packed = Array.from({ length: count }, () => "");
pieces.forEach((piece, index) => {
const slot = Math.min(count - 1, Math.floor(index * count / pieces.length));
packed[slot] = [packed[slot], piece].filter(Boolean).join("");
});
return packed;
}
function compactTraceAssistantMarkdown(text, maxLength) {
const clean = normalizeTraceAssistantMarkdown(text);
if (clean.length <= maxLength) return clean;
return `${clean.slice(0, Math.max(0, maxLength - 12)).trimEnd()}\n\n...内容已截断`;
}
function normalizeTraceAssistantMarkdown(value) {
let text = String(value ?? "")
.replace(/\u0000/gu, "")
.replace(/\r\n|\r/gu, "\n")
.replace(/[ \t]{2,}/gu, " ")
.replace(/\n{3,}/gu, "\n\n")
.trim();
if (!text) return "";
text = restoreTraceAssistantEnglishSpacing(text);
text = restoreTraceAssistantMarkdownBreaks(text);
return text;
}
function restoreTraceAssistantEnglishSpacing(value) {
return String(value ?? "")
.replace(/\bLetmecheckwhat/gu, "Let me check what ")
.replace(/cloudAPI/giu, "Cloud API")
.replace(/CloudAPI/gu, "Cloud API")
.replace(/gatewaysessions/giu, "gateway sessions")
.replace(/gatewaysession/giu, "gateway session")
.replace(/gatewaysidecar/giu, "gateway sidecar")
.replace(/Windows端/gu, "Windows 端")
.replace(/gateway服务/giu, "gateway 服务")
.replace(/gateway恢复/giu, "gateway 恢复")
.replace(/device-podprofilesareavailable/giu, "device-pod profiles are available")
.replace(/profilesareavailable/giu, "profiles are available")
.replace(/\b([A-Za-z]+)areavailable\b/gu, "$1 are available")
.replace(/(\d+)Error\(s\),\s*(\d+)Warning\(s\)/gu, "$1 Error(s), $2 Warning(s)")
.replace(/所有(\d+)个/gu, "所有 $1 个")
.replace(/(\d+)个device-pod/giu, "$1 个 device-pod")
.replace(/个device-pod/giu, "个 device-pod")
.replace(/(\*\*\d+)个/gu, "$1 个")
.replace(/([\u4e00-\u9fff])`/gu, "$1 `")
.replace(/`([^`]+)`([\u4e00-\u9fff])/gu, "`$1` $2")
.replace(/([A-Za-z0-9_-])`/gu, "$1 `")
.replace(/([\u4e00-\u9fff])\*\*/gu, "$1 **")
.replace(/\*\*([\u4e00-\u9fff])/gu, "** $1")
.replace(/([a-z])([\u4e00-\u9fff])/gu, "$1 $2")
.replace(/([\u4e00-\u9fff])([A-Za-z][A-Za-z0-9_-]*)/gu, "$1 $2");
}
function restoreTraceAssistantMarkdownBreaks(value) {
return String(value ?? "")
.replace(/([:。.!?])\s*(\|[^\n]+\|)/gu, "$1\n\n$2")
.replace(/\|\s*\|(?=(?:---|:---|[^|\n]+\s*\|))/gu, "|\n|")
.replace(/\s+-\s+(?=(?:\*\*|`|[\u4e00-\u9fffA-Za-z0-9]))/gu, "\n- ")
.replace(/([^\n])\s+(想对哪个pod进行操作?)/gu, "$1\n\n$2")
.replace(/([^\n])\s+(编译干净通过,无错误无警告。)/gu, "$1\n\n$2");
}
function traceDisplayRow(trace, event, options = {}) {
if (!options.includeNoise && isNoisyTraceEvent(event)) return null;
if (isCommandExecutionTraceEvent(event)) return traceCommandExecutionRow(trace, event);
const clock = traceClock(event.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, event));
const status = traceStatusToken(event);
const label = readableTraceLabel(event);
const meta = [
status,
event.errorCode ? `error=${event.errorCode}` : null,
typeof event.timeoutMs === "number" ? `timeout=${formatTraceDuration(event.timeoutMs)}` : null,
typeof event.idleMs === "number" ? `idle=${formatTraceDuration(event.idleMs)}` : null,
event.outputSummary ? `out=${compactTraceSize(event.outputSummary)}` : null
].filter(Boolean).join(" ");
const body = traceDisplayBody(event);
return {
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`,
seq: event.seq ?? null,
tone: traceEventTone(event),
header: `${clock} total=${total}${meta ? ` ${meta}` : ""}${label ? ` ${label}` : ""}`,
body
};
}
function traceCommandExecutionRow(trace, event) {
const clock = traceClock(event.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, event));
const status = traceStatusToken(event);
const command = cleanTraceOutputText(event.command ?? "");
const meta = [
status,
"tool cmd",
command ? `:: ${compactTraceTextTail(command, 220)}` : null,
Number.isInteger(event.exitCode) ? `exit=${event.exitCode}` : null,
typeof event.durationMs === "number" ? `s=${(event.durationMs / 1000).toFixed(1)}` : null,
typeof event.outputBytes === "number" ? `out=${formatTraceBytes(event.outputBytes)}` : null,
event.itemId ? `item=${event.itemId}` : null
].filter(Boolean).join(" ");
return {
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "command"}:${event.createdAt ?? "unknown"}`}`,
seq: event.seq ?? null,
tone: traceEventTone(event),
header: `${clock} total=${total} ${meta}`,
body: traceCommandExecutionBody(event)
};
}
function traceCommandExecutionBody(event) {
const lines = [
event.command ? `command=${compactTraceTextTail(cleanTraceOutputText(event.command), 900)}` : null,
event.stdoutSummary ? `stdout:\n${compactTraceTextTail(cleanTraceOutputText(event.stdoutSummary), 1200)}` : null,
event.stderrSummary ? `stderr:\n${compactTraceTextTail(cleanTraceOutputText(event.stderrSummary), 800)}` : null,
event.waitingFor && event.status !== "completed" ? `waiting=${event.waitingFor}` : null
].filter(Boolean);
return lines.join("\n");
}
function isCommandExecutionTraceEvent(event) {
return event?.type === "tool_call" && event?.toolName === "commandExecution" &&
(event?.label === "item/commandExecution:started" || event?.label === "item/commandExecution:completed");
}
function isAssistantMessageTraceEvent(event) {
const label = String(event?.label ?? "");
return event?.type === "assistant_message" || label.startsWith("assistant:");
}
function isTerminalAssistantTraceEvent(event) {
const label = String(event?.label ?? "");
return event?.terminal === true || label === "assistant:completed";
}
function isRequestTraceEvent(event) {
const label = String(event?.label ?? "");
return label === "request:accepted-short-connection" ||
label === "request:accepted" ||
label === "request:received";
}
function isSetupTraceEvent(event) {
const label = String(event?.label ?? "");
return /^session:|^stdio:|^prompt:|^thread:|^turn:started|^turn:start|turn\/start|codex turn|tool:codex-app-server/iu.test(label);
}
function isCompletionTraceEvent(event) {
const label = String(event?.label ?? "");
return label.startsWith("turn:completed") || label.startsWith("result:completed");
}
function tracePromptText(event) {
return cleanTraceText(event?.promptSummary ?? event?.prompt ?? "");
}
function traceNonEmptyString(value) {
const text = String(value ?? "").trim();
return text ? text : null;
}
function isNoisyTraceEvent(event) {
const label = String(event?.label ?? "");
if (isToolOutputChunkTraceEvent(event)) return false;
if (/token_count|outputDelta:chunk/iu.test(label)) return true;
if (event?.type === "event" && !event.outputSummary && !event.message && !event.errorCode) return true;
return false;
}
function isToolOutputChunkTraceEvent(event) {
const label = String(event?.label ?? "");
return label === "item/commandExecution/outputDelta" ||
(event?.type === "tool_call" && event?.status === "output_chunk" && !String(event?.toolName ?? "").startsWith("codex-app-server"));
}
function readableTraceLabel(event) {
const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`);
if (label === "request:accepted-short-connection") return "submit short-connection";
if (label === "request:accepted") return "request accepted";
if (label.startsWith("tool:codex-app-server.thread/start+turn/start")) return "codex turn start";
if (label.startsWith("tool:codex-app-server.thread/resume+turn/start")) return "codex turn resume";
if (label === "item/commandExecution:started") return "cmd started";
if (label === "item/commandExecution:completed") return "cmd completed";
if (label.startsWith("item/commandExecution/outputDelta")) return "cmd output";
if (label === "turn:waiting:first_assistant_token") return "waiting first assistant token";
if (label.startsWith("turn:completed")) return "turn completed";
if (label.startsWith("result:completed")) return "result ready";
if (label.startsWith("result:failed")) return "result failed";
if (label.startsWith("timeout:")) return "timeout";
if (label.startsWith("cancel:")) return "cancel";
return label
.replace(/^tool:/u, "")
.replace(/^assistant:/u, "assistant ")
.replace(/:completed:completed$/u, " completed")
.replace(/:completed$/u, " completed")
.replace(/:started$/u, " started");
}
function traceDisplayBody(event) {
const lines = [
event.promptSummary ? cleanTraceText(event.promptSummary) : null,
event.outputSummary ? cleanTraceOutputText(event.outputSummary) : null,
event.message ? cleanTraceText(event.message) : null,
event.chunk && !isNoisyTraceEvent(event) ? cleanTraceOutputText(event.chunk) : null,
event.waitingFor && event.status !== "completed" ? `waiting=${event.waitingFor}` : null
].filter(Boolean);
return lines.join("\n").slice(0, 1400);
}
function rawTraceOutputText(event) {
return String(event?.outputSummary ?? event?.chunk ?? event?.message ?? "");
}
function parseGatewayJsonRpcOutputs(text) {
const payloads = parseJsonObjects(text);
return payloads
.filter((payload) => payload && payload.jsonrpc === "2.0" && payload.result && typeof payload.result === "object")
.map((payload) => {
const result = payload.result;
const dispatch = result.dispatch && typeof result.dispatch === "object" ? result.dispatch : {};
if (!("gatewaySessionId" in result) && !("capabilityId" in result) && !("shellExecuted" in dispatch)) return null;
return { payload, result, dispatch };
})
.filter(Boolean);
}
function parseGatewayJsonRpcOutput(text) {
return parseGatewayJsonRpcOutputs(text)[0] ?? null;
}
function parseJsonObjects(text) {
const source = String(text ?? "");
const values = [];
let searchFrom = 0;
while (searchFrom < source.length) {
const start = source.indexOf("{", searchFrom);
if (start < 0) break;
const end = jsonObjectEnd(source, start);
if (end < 0) break;
try {
values.push(JSON.parse(source.slice(start, end + 1)));
searchFrom = end + 1;
} catch {
searchFrom = start + 1;
}
}
return values;
}
function parseFirstJsonObject(text) {
const source = String(text ?? "");
const start = source.indexOf("{");
if (start < 0) return null;
const end = jsonObjectEnd(source, start);
if (end < 0) return null;
try {
return JSON.parse(source.slice(start, end + 1));
} catch {
return null;
}
}
function jsonObjectEnd(source, start) {
let depth = 0;
let inString = false;
let escaped = false;
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (inString) {
if (escaped) {
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === "\"") {
inString = false;
}
continue;
}
if (char === "\"") {
inString = true;
continue;
}
if (char === "{") depth += 1;
if (char === "}") depth -= 1;
if (depth === 0) {
return index;
}
}
return -1;
}
function gatewayJsonRpcDisplayBody({ payload, result, dispatch, raw }) {
const stdout = cleanTraceOutputText(dispatch.stdout);
const stderr = cleanTraceOutputText(dispatch.stderr);
const error = dispatch.error?.message ?? dispatch.error ?? dispatch.message ?? result.message ?? null;
return [
`request=${payload.id ?? "unknown"}`,
`gateway=${result.gatewaySessionId ?? "unknown"} resource=${result.resourceId ?? "unknown"} capability=${result.capabilityId ?? "unknown"}`,
`dispatch=${dispatch.dispatchStatus ?? result.status ?? "unknown"} shellExecuted=${dispatch.shellExecuted === true ? "true" : "false"} timedOut=${dispatch.timedOut === true ? "true" : "false"}`,
error ? `error=${compactTraceTextTail(cleanTraceOutputText(error), 600)}` : null,
dispatch.command ? `command=${compactTraceTextTail(cleanTraceOutputText(dispatch.command), 420)}` : null,
result.auditId ? `audit=${result.auditId}` : null,
result.evidenceId ? `evidence=${result.evidenceId}` : null,
dispatch.stdoutTruncated === true ? "stdoutTruncated=true" : null,
stdout ? `stdout:\n${compactTraceTextTail(stdout, 1200)}` : null,
stderr ? `stderr:\n${compactTraceTextTail(stderr, 800)}` : null,
!stdout && !stderr && raw ? compactTraceTextTail(cleanTraceOutputText(raw), 900) : null
].filter(Boolean).join("\n");
}
function traceStatusToken(event) {
const status = String(event?.status ?? "");
if (status === "completed" || status === "succeeded") return "ok";
if (["failed", "blocked", "error", "timeout", "canceled"].includes(status) || event?.errorCode) return "fail";
if (["started", "running", "accepted"].includes(status)) return "run";
return status || "event";
}
function traceEventTone(event) {
const status = traceStatusToken(event);
if (status === "ok") return "ok";
if (status === "fail") return "blocked";
if (status === "run") return "warn";
return "source";
}
function traceClock(value) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? "--:--:--" : date.toISOString().slice(11, 19);
}
function traceRelativeMs(trace, event) {
if (typeof event?.elapsedMs === "number") return event.elapsedMs;
const start = Date.parse(trace?.startedAt ?? trace?.createdAt ?? "");
const current = Date.parse(event?.createdAt ?? "");
return Number.isNaN(start) || Number.isNaN(current) ? 0 : Math.max(0, current - start);
}
function formatTraceDuration(ms) {
const total = Math.max(0, Math.floor(Number(ms) || 0));
const seconds = Math.floor(total / 1000);
const hh = String(Math.floor(seconds / 3600)).padStart(2, "0");
const mm = String(Math.floor((seconds % 3600) / 60)).padStart(2, "0");
const ss = String(seconds % 60).padStart(2, "0");
return `${hh}:${mm}:${ss}`;
}
function compactTraceSize(value) {
const length = String(value ?? "").length;
if (length >= 1000) return `${(length / 1000).toFixed(length >= 10000 ? 0 : 1)}k`;
return String(length);
}
function formatTraceBytes(value) {
const bytes = Math.max(0, Number(value) || 0);
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MiB`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KiB`;
return `${bytes}B`;
}
function cleanTraceText(value) {
const text = String(value ?? "").trim();
if (!text) return "";
return text
.replace(/\s*\/\s*/gu, " / ")
.replace(/[ \t]{2,}/gu, " ")
.replace(/\n{3,}/gu, "\n\n");
}
function cleanTraceOutputText(value) {
const text = String(value ?? "").trim();
if (!text) return "";
return text
.replace(/[ \t]{2,}/gu, " ")
.replace(/\n{3,}/gu, "\n\n");
}
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: Number.isInteger(trace?.eventCount) ? trace.eventCount : events.length,
eventsCompacted: trace?.eventsCompacted === true,
eventWindow: trace?.eventWindow ?? null,
lastEvent: trace?.lastEvent ?? events.at(-1) ?? null,
assistantStreams: Array.isArray(trace?.assistantStreams) ? trace.assistantStreams : [],
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 = Number.isInteger(trace?.eventCount) ? trace.eventCount : Array.isArray(trace?.events) ? trace.events.length : 0;
const last = trace?.lastEvent?.label ?? trace?.eventLabels?.at?.(-1) ?? "等待事件";
const elapsed = typeof trace?.elapsedMs === "number" ? ` / ${trace.elapsedMs}ms` : "";
const waiting = trace?.waitingFor ? ` / 等待 ${trace.waitingFor}` : "";
const thread = trace?.threadId ?? message.threadId ? ` / thread=${trace?.threadId ?? message.threadId}` : "";
return `运行 trace ${trace?.traceId ?? message.traceId ?? "pending"} / events=${count} / last=${last}${elapsed}${waiting}${thread}`;
}