fix(web): 保留 trace REST 权威事件

This commit is contained in:
lyon
2026-06-17 07:51:00 +08:00
parent f038119699
commit 68cd9f0ced
3 changed files with 58 additions and 11 deletions
@@ -14,7 +14,10 @@ const expanded = ref(false);
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
const readableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[]));
const emptyTraceLabel = computed(() => props.trace?.fullTraceLoaded === true ? "思考中..." : "加载中...");
const emptyTraceLabel = computed(() => {
if (events.value.length > 0 || (props.trace?.fullTraceLoaded === true && eventCount.value > 0)) return "暂无可读 Trace";
return props.trace?.fullTraceLoaded === true ? "思考中..." : "加载中...";
});
const toolPreviewLimit = 140;
watch(() => [eventCount.value, props.trace?.status, following.value, expanded.value], async () => {
@@ -89,11 +89,13 @@ export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: Non
if (!previous) return next;
const previousEvents = Array.isArray(previous.events) ? previous.events : [];
const nextEvents = Array.isArray(next.events) ? next.events : [];
const nextAuthoritative = next.fullTraceLoaded === true || next.eventSource === "trace-api";
const previousAuthoritative = previous.fullTraceLoaded === true || previous.eventSource === "trace-api";
const keepPreviousEvents = previousAuthoritative && !nextAuthoritative && (next.eventsCompacted === true || nextEvents.length < previousEvents.length);
const nextAuthoritative = next.eventSource === "trace-api";
const previousAuthoritative = previous.eventSource === "trace-api";
const keepPreviousEvents = previousEvents.length > 0 && !nextAuthoritative && (previousAuthoritative || next.eventsCompacted === true || nextEvents.length <= previousEvents.length);
const mergeAuthoritativeEvents = nextAuthoritative && previousEvents.length > 0 && (next.hasMore === true || next.fullTraceLoaded !== true || previous.fullTraceLoaded !== true || nextEvents.length < previousEvents.length);
const events = nextAuthoritative ? mergeAuthoritativeEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents : keepPreviousEvents ? previousEvents : nextEvents.length >= previousEvents.length ? nextEvents : mergeTraceEvents(previousEvents, nextEvents);
const events = nextAuthoritative
? mergeAuthoritativeEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents
: keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents;
const eventCount = keepPreviousEvents ? previous.eventCount ?? events.length : next.eventCount ?? previous.eventCount ?? events.length;
return { ...previous, ...next, events, eventCount, updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString() };
}
+48 -6
View File
@@ -39,6 +39,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const workspaceSelectionEpoch = ref(0);
const sessionStatusAuthority = ref<Record<string, SessionStatusAuthority>>({});
const turnStatusAuthority = ref<Record<string, TurnStatusAuthority>>({});
const traceAuthorityById = ref<Record<string, NonNullable<ChatMessage["runnerTrace"]>>>({});
const traceHydrationInFlight = new Set<string>();
const visibleConversations = computed(() => conversations.value);
@@ -103,7 +104,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
workspace.value = workspaceSnapshot;
providerProfile.value = firstNonEmptyString(workspace.value?.providerProfile, workspace.value?.workspace?.providerProfile, providerProfile.value) ?? providerProfile.value;
rememberWorkbenchProjectId(workspaceProjectId(workspace.value, nextProjectId));
messages.value = messagesFromSelectedConversation(selectedConversation, selectedConversationId, previousConversationId, messages.value);
messages.value = restoreMessagesTraceAuthority(messagesFromSelectedConversation(selectedConversation, selectedConversationId, previousConversationId, messages.value), messages.value);
void hydrateTurnStatusAuthority(messages.value);
void hydrateTerminalMessageDiagnostics();
void hydrateTraceEvents(messages.value);
@@ -133,7 +134,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
loading.value = false;
if (response.ok && isCurrentWorkspaceSelection(requestEpoch)) {
workspace.value = response.data?.workspace ?? current;
messages.value = messagesFromWorkspace(workspace.value);
messages.value = restoreMessagesTraceAuthority(messagesFromWorkspace(workspace.value), messages.value);
currentRequest.value = null;
void hydrateTurnStatusAuthority(messages.value);
void hydrateTraceEvents(messages.value);
@@ -165,7 +166,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
if (!selectedConversation) error.value = detailResponse.error ?? "conversation unavailable";
const selectedProjectId = conversationProjectId(selectedConversation ?? conversation, tabProjectId);
workspace.value = workspaceWithSelectedConversation(response.data?.workspace ?? current, selectedConversation ?? conversation, selectedProjectId);
messages.value = messagesFromSelectedConversation(selectedConversation, conversation.conversationId, null, []);
messages.value = restoreMessagesTraceAuthority(messagesFromSelectedConversation(selectedConversation, conversation.conversationId, null, []), messages.value);
if (selectedConversation) conversations.value = mergeConversationIntoList(conversations.value, selectedConversation);
void hydrateTurnStatusAuthority(messages.value);
void hydrateTerminalMessageDiagnostics();
@@ -214,7 +215,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const response = await api.workbench.deleteConversation(conversationId, { projectId: activeProjectId.value, workspaceId: workspace.value?.workspaceId, updatedByClient: "cloud-web-vue" });
if (response.ok) {
workspace.value = response.data?.workspace ?? workspace.value;
messages.value = messagesFromWorkspace(workspace.value);
messages.value = restoreMessagesTraceAuthority(messagesFromWorkspace(workspace.value), messages.value);
currentRequest.value = null;
void hydrateTurnStatusAuthority(messages.value);
await refreshConversations(selectedConversationIdFromWorkspace(workspace.value));
@@ -321,6 +322,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
messages.value = messages.value.map((message) => {
if (message.traceId !== traceId || message.role !== "agent") return message;
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result as AgentChatResultResponse);
rememberTraceAuthority(runnerTrace);
const error = normalizeAgentError((result as AgentChatResultResponse).error ?? runnerTrace?.error ?? message.error);
const errorText = agentErrorDisplayText(error);
const agentRun = agentRunFromResult(result as AgentChatResultResponse, runnerTrace) ?? agentRunFromMessage(message);
@@ -446,6 +448,32 @@ export const useWorkbenchStore = defineStore("workbench", () => {
await Promise.all(source.filter(messageNeedsTraceHydration).slice(-12).map((message) => hydrateTraceEventsForMessage(message)));
}
function restoreMessagesTraceAuthority(source: ChatMessage[], previous: ChatMessage[] = []): ChatMessage[] {
const previousTraceById = new Map<string, NonNullable<ChatMessage["runnerTrace"]>>();
for (const message of previous) {
const trace = message.runnerTrace;
const traceId = firstNonEmptyString(message.traceId, trace?.traceId);
if (!traceId || !trace || !traceHasEvents(trace)) continue;
previousTraceById.set(traceId, trace);
}
return source.map((message) => {
if (message.role !== "agent") return message;
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
const authority = traceId ? traceAuthorityById.value[traceId] ?? previousTraceById.get(traceId) ?? null : null;
if (!authority) return message;
return { ...message, runnerTrace: mergeRunnerTrace(message.runnerTrace, authority) };
});
}
function rememberTraceAuthority(trace: ChatMessage["runnerTrace"]): void {
const traceId = firstNonEmptyString(trace?.traceId);
if (!traceId || !trace) return;
const existing = traceAuthorityById.value[traceId] ?? null;
if (trace.eventSource !== "trace-api" && !traceHasEvents(trace) && !existing) return;
const nextTrace = mergeRunnerTrace(existing, trace as NonNullable<ChatMessage["runnerTrace"]>);
traceAuthorityById.value = { ...traceAuthorityById.value, [traceId]: nextTrace };
}
function applyTraceHydrationResult(traceId: string, result: AgentChatResultResponse): void {
const events = Array.isArray(result.events) ? result.events : Array.isArray(result.traceEvents) ? result.traceEvents : [];
messages.value = messages.value.map((message) => {
@@ -457,6 +485,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
sessionId: result.sessionId ?? message.runnerTrace?.sessionId,
threadId: result.threadId ?? message.runnerTrace?.threadId,
events,
eventSource: "trace-api",
eventCount: result.eventCount ?? events.length,
eventsCompacted: result.runnerTrace?.eventsCompacted ?? false,
fullTraceLoaded: result.fullTraceLoaded === true || result.hasMore === false,
@@ -480,6 +509,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const errorText = agentErrorDisplayText(error);
const text = terminal ? firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), agentReplyText(result.reply), errorText, result.text, result.summary, message.text) ?? message.text : message.text;
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
rememberTraceAuthority(runnerTrace);
return { ...message, status, text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
});
}
@@ -549,7 +579,12 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const trace = snapshotToRunnerTrace(snapshot);
rememberTurnStatus(traceId, snapshot);
const status = statusFromResult(snapshot.status);
messages.value = messages.value.map((message) => message.traceId === traceId ? { ...message, status, traceAutoLifecycle: "running", runnerTrace: mergeRunnerTrace(message.runnerTrace, trace), updatedAt: new Date().toISOString() } : message);
messages.value = messages.value.map((message) => {
if (message.traceId !== traceId) return message;
const runnerTrace = mergeRunnerTrace(message.runnerTrace, trace);
rememberTraceAuthority(runnerTrace);
return { ...message, status, traceAutoLifecycle: "running", runnerTrace, updatedAt: new Date().toISOString() };
});
void refreshSessionStatusById(trace.sessionId ?? selectedSessionId.value);
}
@@ -559,6 +594,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
messages.value = messages.value.map((message) => {
if (message.traceId !== traceId || message.role !== "agent") return message;
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
rememberTraceAuthority(runnerTrace);
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
return { ...message, status: terminalStatus, text, traceAutoLifecycle: "terminal", runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
@@ -588,6 +624,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
messages.value = messages.value.map((message) => {
if (message.traceId !== traceId || message.role !== "agent") return message;
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
rememberTraceAuthority(runnerTrace);
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
const status = statusFromResult(result.status);
@@ -870,10 +907,15 @@ function messageNeedsTraceHydration(message: ChatMessage): boolean {
if (!traceId) return false;
const trace = message.runnerTrace;
const events = Array.isArray(trace?.events) ? trace.events : [];
if (trace?.fullTraceLoaded === true && trace.eventsCompacted !== true) return false;
const eventCount = firstFiniteNumber(trace?.eventCount) ?? events.length;
if (trace?.fullTraceLoaded === true && trace.eventsCompacted !== true) return events.length === 0 && (eventCount > 0 || isTraceActiveStatus(trace?.status) || isTraceActiveStatus(message.status));
return events.length === 0 || trace?.eventsCompacted === true || trace?.fullTraceLoaded !== true;
}
function traceHasEvents(trace: ChatMessage["runnerTrace"]): boolean {
return Array.isArray(trace?.events) && trace.events.length > 0;
}
function agentRunFromResult(result: AgentChatResultResponse, runnerTrace: ChatMessage["runnerTrace"]): AgentRunProvenance | null {
return asAgentRun(result.agentRun ?? runnerTrace?.agentRun);
}