From 933d97bedcabc43dac2aa384fe6281052bf9cb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E9=9D=A9?= <507639721@qq.com> Date: Thu, 26 Mar 2026 13:56:50 +0800 Subject: [PATCH] fix(plan-notebook): buffer SSE stream lines to preserve order --- .../src/main/resources/static/index.html | 61 +++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/agentscope-examples/plan-notebook/src/main/resources/static/index.html b/agentscope-examples/plan-notebook/src/main/resources/static/index.html index 6a2c48cac..66c744550 100644 --- a/agentscope-examples/plan-notebook/src/main/resources/static/index.html +++ b/agentscope-examples/plan-notebook/src/main/resources/static/index.html @@ -1103,34 +1103,59 @@

Subtasks

const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullText = ''; + // SSE can be split across chunks; keep an incremental buffer so we only + // process complete lines like `data: xxx`. + let buffer = ''; + + const handleData = (data) => { + if (data === '[PAUSED]') { + isPaused = true; + stopRequested = false; + controlState = 'continue'; + updateControlUI(); + if (!fullText) { + fullText = '(Plan updated - Review and edit the plan, then Continue)'; + } + } else { + fullText += data; + } + contentDiv.textContent = fullText; + }; while (true) { const { done, value } = await reader.read(); if (done) break; - const chunk = decoder.decode(value); - const lines = chunk.split('\n'); + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + + const lines = buffer.split('\n'); + // Keep the last (possibly incomplete) line for the next chunk. + buffer = lines.pop(); + for (const line of lines) { - if (line.startsWith('data:')) { - const data = line.substring(5).trim(); - if (data) { - if (data === '[PAUSED]') { - isPaused = true; - stopRequested = false; - controlState = 'continue'; - updateControlUI(); - if (!fullText) { - fullText = '(Plan updated - Review and edit the plan, then Continue)'; - } - } else { - fullText += data; - } - contentDiv.textContent = fullText; - } + const lineClean = line.trimEnd(); + if (!lineClean.startsWith('data:')) continue; + + const data = lineClean.substring(5).trim(); + if (data) { + handleData(data); } } } + // Flush any remaining decoded text and process the final buffered line + // (in case the stream ends without a trailing newline). + const remaining = decoder.decode(); + if (remaining) buffer += remaining; + if (buffer) { + const lineClean = buffer.trimEnd(); + if (lineClean.startsWith('data:')) { + const data = lineClean.substring(5).trim(); + if (data) handleData(data); + } + } + if (!fullText) { contentDiv.textContent = defaultMessage; }