mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-14 13:15:07 +08:00
Merge pull request #375 from CJackHwang/codex/investigate-data-loss-issue-in-pr-369
sse/parser: treat object-shaped `v` as visible content, preserve INCOMPLETE across omitted status; add tests and samples
This commit is contained in:
@@ -219,3 +219,33 @@ func (d failingOrCompletionDoer) Do(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
return nil, errors.New("forced stream failure")
|
||||
}
|
||||
|
||||
func TestAutoContinuePreservesIncompleteStateWhenNextChunkOmitsStatus(t *testing.T) {
|
||||
initialBody := strings.Join([]string{
|
||||
`data: {"response_message_id":321,"v":{"response":{"message_id":321,"status":"INCOMPLETE"}}}`,
|
||||
`data: {"p":"response/content","v":{"text":"continued"}}`,
|
||||
`data: [DONE]`,
|
||||
}, "\n") + "\n"
|
||||
|
||||
var continueCalls atomic.Int32
|
||||
body := newAutoContinueBody(context.Background(), io.NopCloser(strings.NewReader(initialBody)), "session-123", 8, func(context.Context, string, int) (*http.Response, error) {
|
||||
continueCalls.Add(1)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(
|
||||
`data: {"response_message_id":322,"p":"response/status","v":"FINISHED"}` + "\n" +
|
||||
`data: [DONE]` + "\n",
|
||||
)),
|
||||
}, nil
|
||||
})
|
||||
defer func() { _ = body.Close() }()
|
||||
|
||||
_, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body failed: %v", err)
|
||||
}
|
||||
if continueCalls.Load() != 1 {
|
||||
t.Fatalf("expected exactly one continue call, got %d", continueCalls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"client": {
|
||||
"name": "DeepSeek",
|
||||
"platform": "android",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.3",
|
||||
"android_api_level": "35",
|
||||
"locale": "zh_CN"
|
||||
},
|
||||
|
||||
@@ -244,11 +244,29 @@ func appendChunkValueContent(v any, partType string, newType *string, parts *[]C
|
||||
}
|
||||
*parts = append(*parts, pp...)
|
||||
case map[string]any:
|
||||
if appendObjectContentByPath(path, val, partType, parts) {
|
||||
return false
|
||||
}
|
||||
appendWrappedFragments(val, partType, newType, parts)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendObjectContentByPath(path string, val map[string]any, partType string, parts *[]ContentPart) bool {
|
||||
if path != "response/content" && path != "response/thinking_content" && path != "" {
|
||||
return false
|
||||
}
|
||||
text, _ := val["text"].(string)
|
||||
if text == "" {
|
||||
text, _ = val["content"].(string)
|
||||
}
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
appendContentPart(parts, text, partType)
|
||||
return true
|
||||
}
|
||||
|
||||
func appendWrappedFragments(val map[string]any, partType string, newType *string, parts *[]ContentPart) {
|
||||
resp := val
|
||||
if wrapped, ok := val["response"].(map[string]any); ok {
|
||||
|
||||
@@ -163,3 +163,44 @@ func TestParseSSEChunkForContentStripsLeakedThinkTagsFromText(t *testing.T) {
|
||||
t.Fatalf("expected leaked think tag to be stripped, got %#v", parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSSEChunkForContentResponseContentObjectShape(t *testing.T) {
|
||||
chunk := map[string]any{
|
||||
"p": "response/content",
|
||||
"v": map[string]any{"text": "对象内容"},
|
||||
}
|
||||
parts, finished, _ := ParseSSEChunkForContent(chunk, false, "text")
|
||||
if finished {
|
||||
t.Fatal("expected unfinished")
|
||||
}
|
||||
if len(parts) != 1 || parts[0].Text != "对象内容" || parts[0].Type != "text" {
|
||||
t.Fatalf("unexpected parts: %#v", parts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSSEChunkForThinkingContentObjectShape(t *testing.T) {
|
||||
chunk := map[string]any{
|
||||
"p": "response/thinking_content",
|
||||
"v": map[string]any{"content": "对象思考"},
|
||||
}
|
||||
parts, finished, _ := ParseSSEChunkForContent(chunk, true, "thinking")
|
||||
if finished {
|
||||
t.Fatal("expected unfinished")
|
||||
}
|
||||
if len(parts) != 1 || parts[0].Text != "对象思考" || parts[0].Type != "thinking" {
|
||||
t.Fatalf("unexpected parts: %#v", parts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSSEChunkForContentObjectShapeWithoutPath(t *testing.T) {
|
||||
chunk := map[string]any{
|
||||
"v": map[string]any{"text": "无路径对象内容"},
|
||||
}
|
||||
parts, finished, _ := ParseSSEChunkForContent(chunk, false, "text")
|
||||
if finished {
|
||||
t.Fatal("expected unfinished")
|
||||
}
|
||||
if len(parts) != 1 || parts[0].Text != "无路径对象内容" || parts[0].Type != "text" {
|
||||
t.Fatalf("unexpected parts: %#v", parts)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user