修复吞字问题

This commit is contained in:
CJACK
2026-05-01 01:31:48 +08:00
parent fca8c01397
commit 92e321fe2c
11 changed files with 257 additions and 47 deletions

View File

@@ -92,6 +92,7 @@ func ParseSSEChunkForContentDetailed(chunk map[string]any, thinkingEnabled bool,
}
newType := currentFragmentType
parts := make([]ContentPart, 0, 8)
updateTypeFromExplicitPath(path, thinkingEnabled, &newType)
collectDirectFragments(path, chunk, v, &newType, &parts)
updateTypeFromNestedResponse(path, v, &newType)
partType := resolvePartType(path, thinkingEnabled, newType)
@@ -107,11 +108,24 @@ func ParseSSEChunkForContentDetailed(chunk map[string]any, thinkingEnabled bool,
detectionThinkingParts := selectThinkingParts(parts)
if !thinkingEnabled {
parts = dropThinkingParts(parts)
newType = "text"
}
return parts, detectionThinkingParts, false, newType
}
func updateTypeFromExplicitPath(path string, thinkingEnabled bool, newType *string) {
if newType == nil {
return
}
switch path {
case "response/content":
*newType = "text"
case "response/thinking_content":
if !thinkingEnabled || *newType != "text" {
*newType = "thinking"
}
}
}
func selectThinkingParts(parts []ContentPart) []ContentPart {
if len(parts) == 0 {
return nil
@@ -206,8 +220,11 @@ func resolvePartType(path string, thinkingEnabled bool, newType string) string {
return "text"
case strings.Contains(path, "response/fragments") && strings.Contains(path, "/content"):
return newType
case path == "" && thinkingEnabled:
return newType
case path == "":
if newType != "" {
return newType
}
return "text"
default:
return "text"
}

View File

@@ -88,6 +88,71 @@ func TestParseSSEChunkForContentAfterAppendUsesUpdatedType(t *testing.T) {
}
}
func TestParseSSEChunkForContentThinkingDisabledKeepsHiddenFragmentState(t *testing.T) {
chunk1 := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": []any{
map[string]any{"type": "THINK", "content": "我们"},
},
}
parts1, finished1, nextType1 := ParseSSEChunkForContent(chunk1, false, "text")
if finished1 {
t.Fatal("expected first chunk unfinished")
}
if nextType1 != "thinking" {
t.Fatalf("expected hidden THINK fragment to keep next type thinking, got %q", nextType1)
}
if len(parts1) != 0 {
t.Fatalf("expected hidden thinking to be dropped, got %#v", parts1)
}
chunk2 := map[string]any{
"p": "response/fragments/-1/content",
"v": "被",
}
parts2, finished2, nextType2 := ParseSSEChunkForContent(chunk2, false, nextType1)
if finished2 {
t.Fatal("expected second chunk unfinished")
}
if nextType2 != "thinking" {
t.Fatalf("expected hidden continuation to keep next type thinking, got %q", nextType2)
}
if len(parts2) != 0 {
t.Fatalf("expected hidden continuation to be dropped, got %#v", parts2)
}
chunk3 := map[string]any{"v": "要求"}
parts3, finished3, nextType3 := ParseSSEChunkForContent(chunk3, false, nextType2)
if finished3 {
t.Fatal("expected third chunk unfinished")
}
if nextType3 != "thinking" {
t.Fatalf("expected pathless hidden continuation to keep next type thinking, got %q", nextType3)
}
if len(parts3) != 0 {
t.Fatalf("expected pathless hidden continuation to be dropped, got %#v", parts3)
}
chunk4 := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": []any{
map[string]any{"type": "RESPONSE", "content": "答"},
},
}
parts4, finished4, nextType4 := ParseSSEChunkForContent(chunk4, false, nextType3)
if finished4 {
t.Fatal("expected fourth chunk unfinished")
}
if nextType4 != "text" {
t.Fatalf("expected RESPONSE fragment to switch next type text, got %q", nextType4)
}
if len(parts4) != 1 || parts4[0].Type != "text" || parts4[0].Text != "答" {
t.Fatalf("expected visible response text, got %#v", parts4)
}
}
func TestParseSSEChunkForContentAutoTransitionsThinkClose(t *testing.T) {
chunk := map[string]any{
"p": "response/thinking_content",

View File

@@ -158,11 +158,13 @@ func TestStartParsedLinePumpNonSSELines(t *testing.T) {
func TestStartParsedLinePumpThinkingDisabled(t *testing.T) {
body := strings.NewReader(
"data: {\"p\":\"response/thinking_content\",\"v\":\"thought\"}\n" +
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"THINK\",\"content\":\"思\"}]}\n" +
"data: {\"p\":\"response/fragments/-1/content\",\"v\":\"考\"}\n" +
"data: {\"v\":\"隐藏\"}\n" +
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"RESPONSE\",\"content\":\"答\"}]}\n" +
"data: {\"p\":\"response/content\",\"v\":\"response\"}\n" +
"data: [DONE]\n",
)
// With thinking disabled, thinking content should still be emitted but marked differently
results, done := StartParsedLinePump(context.Background(), body, false, "text")
var parts []ContentPart
@@ -171,8 +173,15 @@ func TestStartParsedLinePumpThinkingDisabled(t *testing.T) {
}
<-done
if len(parts) < 1 {
t.Fatalf("expected at least 1 part, got %d", len(parts))
got := strings.Builder{}
for _, p := range parts {
if p.Type != "text" {
t.Fatalf("expected only text parts with thinking disabled, got %#v", parts)
}
got.WriteString(p.Text)
}
if got.String() != "答response" {
t.Fatalf("expected hidden thinking to be dropped, got %q from %#v", got.String(), parts)
}
}