fix(sse): correct UTF-8 handling and chunking in SSE streaming\n\n- Updated splitContentIntoSegments to handle runes instead of bytes\n- Fixed buildWebUIStreamForwarder to send delta chunks instead of cumulative content\n- Ensures proper handling of multi-byte characters in SSE streams

This commit is contained in:
whlaoding
2026-03-14 01:41:51 +08:00
parent 60195f00a0
commit ea88e1dc18
7 changed files with 89 additions and 18 deletions

View File

@@ -220,7 +220,7 @@ func runMessageChannel(ctx context.Context, cfg config.Config, engine *agent.Orc
return engine.HandleMessage(ctx, msg.ChatID, msg.UserID, msg.Text)
},
func(ctx context.Context, msg webui.IncomingMessage, callback webui.StreamEventCallback) (string, error) {
return engine.HandleMessageStream(ctx, msg.ChatID, msg.UserID, msg.Text, buildWebUIStreamForwarder(callback))
return engine.HandleMessageStream(ctx, msg.ChatID, msg.UserID, msg.Text, buildWebUIStreamForwarder(callback, cfg.WebUI.ExposeReasoning))
},
func(ctx context.Context, chatID, userID string, files []llm.InputFile) ([]string, error) {
return engine.UploadAndCacheFiles(ctx, chatID, userID, files)
@@ -231,16 +231,54 @@ func runMessageChannel(ctx context.Context, cfg config.Config, engine *agent.Orc
}
}
func buildWebUIStreamForwarder(callback webui.StreamEventCallback) agent.StreamEventCallback {
func buildWebUIStreamForwarder(callback webui.StreamEventCallback, exposeReasoning bool) agent.StreamEventCallback {
const finalChunkRunes = 12
const finalChunkInterval = 25 * time.Millisecond
return func(event agent.StreamEvent) error {
if callback == nil {
return nil
}
switch event.Type {
case agent.StreamEventTypeFinal, agent.StreamEventTypeError:
case agent.StreamEventTypeThought, agent.StreamEventTypeToolCall, agent.StreamEventTypeToolResult:
if !exposeReasoning {
return nil
}
return callback(webui.StreamEvent{
Type: webui.StreamEventType(event.Type),
Type: webui.StreamEventType(event.Type),
Content: event.Content,
Step: event.Step,
ToolName: event.ToolName,
})
case agent.StreamEventTypeFinal:
runes := []rune(event.Content)
if len(runes) == 0 {
return callback(webui.StreamEvent{Type: webui.StreamEventTypeFinal, Content: "", Step: event.Step})
}
start := 0
for start < len(runes) {
end := start + finalChunkRunes
if end > len(runes) {
end = len(runes)
}
if err := callback(webui.StreamEvent{
Type: webui.StreamEventTypeFinal,
Content: string(runes[start:end]),
Step: event.Step,
}); err != nil {
return err
}
start = end
if start < len(runes) {
time.Sleep(finalChunkInterval)
}
}
return nil
case agent.StreamEventTypeError:
return callback(webui.StreamEvent{
Type: webui.StreamEventTypeError,
Content: event.Content,
Step: event.Step,
})
default:
return nil