From 2d5d246606c27c6e58503cb5c163af9f28dcbe32 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Wed, 26 Aug 2026 17:59:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(webui):=20SSE=20writer=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=90=88=E5=B9=B6=20flush=20=E4=BF=AE=E5=A4=8D=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=20delta=20=E4=B8=A2=E5=8C=85=E5=BB=B6=E8=BF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【根因】SSE writeCh 缓冲仅 64 且 writer 每条 delta 单独 flush。 reasoning/content 增量是高频小包(单轮 200+ 条),socket 写慢时 writeCh 迅速填满,delta 大量 DROPPED——浏览器收不到 逐 token 增量,只能等最终 agent_output 整段到达,体感明显延迟。 实测一轮 16s 纯文本回复:content_delta DROPPED 202 次、 reasoning_delta DROPPED 345 次,前端全程无流式渲染。 【修复】 - writeCh 缓冲 64 → 512 - writer 加 16ms 批量合并窗口:窗口内收集的增量一次性 flush, 或满 64 条立即 flush;done 退出前 flush 残留。 flush 次数从 N 降到约 N/64,socket 写压力骤降。 修复后实测 0 DROPPED,delta 全部实时送达前端。 --- internal/plugins/webui/handler.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/internal/plugins/webui/handler.go b/internal/plugins/webui/handler.go index b81270e..f2e5f0e 100644 --- a/internal/plugins/webui/handler.go +++ b/internal/plugins/webui/handler.go @@ -1693,7 +1693,10 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) { // writeCh 不 close:Subscribe 回调闭包持有它,handler 退出后回调仍可能被 // 总线异步触发,close 后再发送会 panic(send on closed channel,生产日志中 // 单日数千次)。writer goroutine 通过 done 退出;发送侧 select on done 防泄漏。 - writeCh := make(chan string, 64) + // 缓冲加大到 512 且 writer 做批量合并:reasoning/content 增量是高频小包, + // 每条单独 flush 会因 socket 写慢而填满小缓冲导致 delta 被丢弃(表现为 + // 前端只能等最终的 agent_output 整段,体感延迟)。 + writeCh := make(chan string, 512) writerDone := make(chan struct{}) go func() { defer func() { @@ -1702,12 +1705,32 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) { } close(writerDone) }() + // 批量合并窗口:16ms 内收集的增量一次性 flush,降 flush 次数、 + // 避免高频小包拖慢 socket 写导致 writeCh 积压丢 delta。 + pending := make([]string, 0, 64) + flushPending := func() { + if len(pending) == 0 { + return + } + for _, line := range pending { + fmt.Fprintf(w, "%s\n", line) + } + flusher.Flush() + pending = pending[:0] + } + flushTicker := time.NewTicker(16 * time.Millisecond) + defer flushTicker.Stop() for { select { case line := <-writeCh: - fmt.Fprintf(w, "%s\n", line) - flusher.Flush() + pending = append(pending, line) + if len(pending) >= 64 { + flushPending() + } + case <-flushTicker.C: + flushPending() case <-done: + flushPending() return } }