diff --git a/frontend/app/app/api/chat/route.ts b/frontend/app/app/api/chat/route.ts index 450131f..4537610 100644 --- a/frontend/app/app/api/chat/route.ts +++ b/frontend/app/app/api/chat/route.ts @@ -53,6 +53,8 @@ type ClawStreamItem = type StreamState = { textStarted: boolean; textStreamed: boolean; + textPartIndex: number; + activeTextId?: string; reasoningEnded: boolean; phase: "waiting" | "queued" | "running" | "done"; runId?: string; @@ -112,6 +114,7 @@ export async function POST(req: Request) { const streamState: StreamState = { textStarted: false, textStreamed: false, + textPartIndex: 0, reasoningEnded: false, phase: "waiting", announcedToolCallKeys: new Set(), @@ -151,13 +154,9 @@ export async function POST(req: Request) { } writeToolTrace(writer, payload.transcript, streamState); if (!streamState.textStreamed) { - writer.write({ type: "text-start", id: "text-1" }); - writer.write({ type: "text-delta", id: "text-1", delta: text }); - writer.write({ type: "text-end", id: "text-1" }); - } else if (streamState.textStarted) { - writer.write({ type: "text-end", id: "text-1" }); - streamState.textStarted = false; + writeTextDelta(writer, text, streamState); } + endTextPart(writer, streamState); writer.write({ type: "finish-step" }); writer.write({ type: "finish", @@ -178,9 +177,9 @@ export async function POST(req: Request) { ? err.message : "请求失败。"; writer.write({ type: "reasoning-end", id: "reasoning-1" }); - writer.write({ type: "text-start", id: "text-1" }); - writer.write({ type: "text-delta", id: "text-1", delta: text }); - writer.write({ type: "text-end", id: "text-1" }); + endTextPart(writer, streamState); + writeTextDelta(writer, text, streamState); + endTextPart(writer, streamState); writer.write({ type: "finish-step" }); writer.write({ type: "finish", @@ -209,6 +208,45 @@ function formatDuration(ms: number) { return seconds > 0 ? `${minutes}min${seconds}s` : `${minutes}min`; } +function startTextPart( + writer: UIMessageStreamWriter, + streamState: StreamState, +) { + if (streamState.textStarted && streamState.activeTextId) { + return streamState.activeTextId; + } + streamState.textPartIndex += 1; + const id = `text-${streamState.textPartIndex}`; + streamState.activeTextId = id; + streamState.textStarted = true; + writer.write({ type: "text-start", id }); + return id; +} + +function writeTextDelta( + writer: UIMessageStreamWriter, + delta: string, + streamState?: StreamState, +) { + if (!streamState) { + writer.write({ type: "text-delta", id: "text-1", delta }); + return; + } + const id = startTextPart(writer, streamState); + writer.write({ type: "text-delta", id, delta }); + streamState.textStreamed = true; +} + +function endTextPart( + writer: UIMessageStreamWriter, + streamState?: StreamState, +) { + if (!streamState?.textStarted || !streamState.activeTextId) return; + writer.write({ type: "text-end", id: streamState.activeTextId }); + streamState.textStarted = false; + streamState.activeTextId = undefined; +} + function writeToolTrace( writer: UIMessageStreamWriter, transcript?: ClawTranscriptEntry[], @@ -232,6 +270,7 @@ function writeToolTrace( if (!toolCallId || !toolName) continue; if (streamState?.streamedToolCallIds.has(toolCallId)) continue; + endTextPart(writer, streamState); writer.write({ type: "tool-input-available", toolCallId, @@ -524,12 +563,7 @@ function writeRuntimeEvent( ); } if (event.type === "content_delta" && event.delta) { - if (!streamState?.textStarted) { - writer.write({ type: "text-start", id: "text-1" }); - if (streamState) streamState.textStarted = true; - } - writer.write({ type: "text-delta", id: "text-1", delta: event.delta }); - if (streamState) streamState.textStreamed = true; + writeTextDelta(writer, event.delta, streamState); } if (event.type === "tool_call_delta") { const key = @@ -539,6 +573,7 @@ function writeRuntimeEvent( : `index:${event.tool_call_index}`); if (key && !streamState?.announcedToolCallKeys.has(key)) { streamState?.announcedToolCallKeys.add(key); + endTextPart(writer, streamState); writeReasoningDelta( writer, { @@ -551,26 +586,16 @@ function writeRuntimeEvent( } } if (event.type === "final_text_start") { - if (!streamState?.textStarted) { - writer.write({ type: "text-start", id: "text-1" }); - if (streamState) streamState.textStarted = true; - } + if (streamState) startTextPart(writer, streamState); } if (event.type === "final_text_delta" && event.delta) { - if (!streamState?.textStarted) { - writer.write({ type: "text-start", id: "text-1" }); - if (streamState) streamState.textStarted = true; - } - writer.write({ type: "text-delta", id: "text-1", delta: event.delta }); - if (streamState) streamState.textStreamed = true; + writeTextDelta(writer, event.delta, streamState); } if (event.type === "final_text_end") { - if (streamState?.textStarted) { - writer.write({ type: "text-end", id: "text-1" }); - streamState.textStarted = false; - } + endTextPart(writer, streamState); } if (event.type === "tool_start" && event.tool_call_id && event.tool_name) { + endTextPart(writer, streamState); streamState?.streamedToolCallIds.add(event.tool_call_id); writer.write({ type: "tool-input-available", @@ -580,6 +605,7 @@ function writeRuntimeEvent( }); } if (event.type === "tool_delta" && event.tool_call_id && event.delta) { + endTextPart(writer, streamState); writeReasoningDelta( writer, {