mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-02 07:25:26 +08:00
225 lines
5.6 KiB
Go
225 lines
5.6 KiB
Go
package sse
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
)
|
|
|
|
type ContentPart struct {
|
|
Text string
|
|
Type string
|
|
}
|
|
|
|
var skipPatterns = []string{
|
|
"quasi_status", "elapsed_secs", "token_usage", "pending_fragment", "conversation_mode",
|
|
"fragments/-1/status", "fragments/-2/status", "fragments/-3/status",
|
|
}
|
|
|
|
func ParseDeepSeekSSELine(raw []byte) (map[string]any, bool, bool) {
|
|
line := strings.TrimSpace(string(raw))
|
|
if line == "" || !strings.HasPrefix(line, "data:") {
|
|
return nil, false, false
|
|
}
|
|
dataStr := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
|
if dataStr == "[DONE]" {
|
|
return nil, true, true
|
|
}
|
|
chunk := map[string]any{}
|
|
if err := json.Unmarshal([]byte(dataStr), &chunk); err != nil {
|
|
return nil, false, false
|
|
}
|
|
return chunk, false, true
|
|
}
|
|
|
|
func shouldSkipPath(path string) bool {
|
|
if path == "response/search_status" {
|
|
return true
|
|
}
|
|
for _, p := range skipPatterns {
|
|
if strings.Contains(path, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ParseSSEChunkForContent(chunk map[string]any, thinkingEnabled bool, currentFragmentType string) ([]ContentPart, bool, string) {
|
|
v, ok := chunk["v"]
|
|
if !ok {
|
|
return nil, false, currentFragmentType
|
|
}
|
|
path, _ := chunk["p"].(string)
|
|
if shouldSkipPath(path) {
|
|
return nil, false, currentFragmentType
|
|
}
|
|
if path == "response/status" {
|
|
if s, ok := v.(string); ok && s == "FINISHED" {
|
|
return nil, true, currentFragmentType
|
|
}
|
|
}
|
|
newType := currentFragmentType
|
|
if path == "response" {
|
|
if arr, ok := v.([]any); ok {
|
|
for _, it := range arr {
|
|
m, ok := it.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if m["p"] == "fragments" && m["o"] == "APPEND" {
|
|
if frags, ok := m["v"].([]any); ok {
|
|
for _, frag := range frags {
|
|
fm, ok := frag.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
t, _ := fm["type"].(string)
|
|
t = strings.ToUpper(t)
|
|
if t == "THINK" || t == "THINKING" {
|
|
newType = "thinking"
|
|
} else if t == "RESPONSE" {
|
|
newType = "text"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
partType := "text"
|
|
switch {
|
|
case path == "response/thinking_content":
|
|
partType = "thinking"
|
|
case path == "response/content":
|
|
partType = "text"
|
|
case strings.Contains(path, "response/fragments") && strings.Contains(path, "/content"):
|
|
partType = newType
|
|
case path == "":
|
|
if thinkingEnabled {
|
|
partType = newType
|
|
}
|
|
}
|
|
parts := make([]ContentPart, 0, 8)
|
|
switch val := v.(type) {
|
|
case string:
|
|
if val == "FINISHED" && (path == "" || path == "status") {
|
|
return nil, true, newType
|
|
}
|
|
if val != "" {
|
|
parts = append(parts, ContentPart{Text: val, Type: partType})
|
|
}
|
|
case []any:
|
|
pp, finished := extractContentRecursive(val, partType)
|
|
if finished {
|
|
return nil, true, newType
|
|
}
|
|
parts = append(parts, pp...)
|
|
case map[string]any:
|
|
resp := val
|
|
if wrapped, ok := val["response"].(map[string]any); ok {
|
|
resp = wrapped
|
|
}
|
|
if frags, ok := resp["fragments"].([]any); ok {
|
|
for _, item := range frags {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
t, _ := m["type"].(string)
|
|
content, _ := m["content"].(string)
|
|
t = strings.ToUpper(t)
|
|
if t == "THINK" || t == "THINKING" {
|
|
newType = "thinking"
|
|
if content != "" {
|
|
parts = append(parts, ContentPart{Text: content, Type: "thinking"})
|
|
}
|
|
} else if t == "RESPONSE" {
|
|
newType = "text"
|
|
if content != "" {
|
|
parts = append(parts, ContentPart{Text: content, Type: "text"})
|
|
}
|
|
} else if content != "" {
|
|
parts = append(parts, ContentPart{Text: content, Type: partType})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return parts, false, newType
|
|
}
|
|
|
|
func extractContentRecursive(items []any, defaultType string) ([]ContentPart, bool) {
|
|
parts := make([]ContentPart, 0, len(items))
|
|
for _, it := range items {
|
|
m, ok := it.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
itemPath, _ := m["p"].(string)
|
|
itemV, hasV := m["v"]
|
|
if !hasV {
|
|
continue
|
|
}
|
|
if itemPath == "status" {
|
|
if s, ok := itemV.(string); ok && s == "FINISHED" {
|
|
return nil, true
|
|
}
|
|
}
|
|
if shouldSkipPath(itemPath) {
|
|
continue
|
|
}
|
|
if content, ok := m["content"].(string); ok && content != "" {
|
|
typeName, _ := m["type"].(string)
|
|
typeName = strings.ToUpper(typeName)
|
|
switch typeName {
|
|
case "THINK", "THINKING":
|
|
parts = append(parts, ContentPart{Text: content, Type: "thinking"})
|
|
case "RESPONSE":
|
|
parts = append(parts, ContentPart{Text: content, Type: "text"})
|
|
default:
|
|
parts = append(parts, ContentPart{Text: content, Type: defaultType})
|
|
}
|
|
continue
|
|
}
|
|
partType := defaultType
|
|
if strings.Contains(itemPath, "thinking") {
|
|
partType = "thinking"
|
|
} else if strings.Contains(itemPath, "content") || itemPath == "response" || itemPath == "fragments" {
|
|
partType = "text"
|
|
}
|
|
switch v := itemV.(type) {
|
|
case string:
|
|
if v != "" && v != "FINISHED" {
|
|
parts = append(parts, ContentPart{Text: v, Type: partType})
|
|
}
|
|
case []any:
|
|
for _, inner := range v {
|
|
switch x := inner.(type) {
|
|
case map[string]any:
|
|
ct, _ := x["content"].(string)
|
|
if ct == "" {
|
|
continue
|
|
}
|
|
typeName, _ := x["type"].(string)
|
|
typeName = strings.ToUpper(typeName)
|
|
if typeName == "THINK" || typeName == "THINKING" {
|
|
parts = append(parts, ContentPart{Text: ct, Type: "thinking"})
|
|
} else if typeName == "RESPONSE" {
|
|
parts = append(parts, ContentPart{Text: ct, Type: "text"})
|
|
} else {
|
|
parts = append(parts, ContentPart{Text: ct, Type: partType})
|
|
}
|
|
case string:
|
|
if x != "" {
|
|
parts = append(parts, ContentPart{Text: x, Type: partType})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return parts, false
|
|
}
|
|
|
|
func IsCitation(text string) bool {
|
|
return bytes.HasPrefix([]byte(strings.TrimSpace(text)), []byte("[citation:"))
|
|
}
|