From 68cd9f0ced5ede381a13094ef9d118de36f869ff Mon Sep 17 00:00:00 2001 From: lyon Date: Wed, 17 Jun 2026 07:51:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E4=BF=9D=E7=95=99=20trace=20REST?= =?UTF-8?q?=20=E6=9D=83=E5=A8=81=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/agent/TraceTimeline.vue | 5 +- .../src/composables/useTraceSubscription.ts | 10 ++-- web/hwlab-cloud-web/src/stores/workbench.ts | 54 ++++++++++++++++--- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue index 460964d9..367a9a21 100644 --- a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue +++ b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue @@ -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[])); -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 () => { diff --git a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts index 4d0296bf..bcb836f4 100644 --- a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts +++ b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts @@ -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() }; } diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index ea5f811b..cf6ef673 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -39,6 +39,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { const workspaceSelectionEpoch = ref(0); const sessionStatusAuthority = ref>({}); const turnStatusAuthority = ref>({}); + const traceAuthorityById = ref>>({}); const traceHydrationInFlight = new Set(); 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>(); + 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); + 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); }