/v1/chat/completions 接口返回报文中出现了[citation:1][citation:2]等未解析的标签,本次改动将返回报文中的标签做了解析

This commit is contained in:
songguoliang
2026-04-20 11:22:31 +08:00
parent 2ba8b143d0
commit d73f7b8b73
9 changed files with 223 additions and 12 deletions

View File

@@ -0,0 +1,124 @@
package sse
import (
"strconv"
"strings"
)
type citationLinkCollector struct {
ordered []string
seen map[string]struct{}
explicit map[int]string
}
func newCitationLinkCollector() *citationLinkCollector {
return &citationLinkCollector{
seen: map[string]struct{}{},
explicit: map[int]string{},
}
}
func (c *citationLinkCollector) ingestChunk(chunk map[string]any) {
if c == nil || len(chunk) == 0 {
return
}
c.walkValue(chunk)
}
func (c *citationLinkCollector) build() map[int]string {
out := make(map[int]string, len(c.explicit)+len(c.ordered))
for idx, u := range c.explicit {
if idx > 0 && strings.TrimSpace(u) != "" {
out[idx] = u
}
}
for i, u := range c.ordered {
idx := i + 1
if _, exists := out[idx]; !exists {
out[idx] = u
}
}
return out
}
func (c *citationLinkCollector) walkValue(v any) {
switch x := v.(type) {
case []any:
for _, item := range x {
c.walkValue(item)
}
case map[string]any:
c.captureURLAndIndex(x)
for _, vv := range x {
c.walkValue(vv)
}
}
}
func (c *citationLinkCollector) captureURLAndIndex(m map[string]any) {
url := strings.TrimSpace(asString(m["url"]))
if !isWebURL(url) {
return
}
c.addOrdered(url)
idx, hasIdx := citationIndexFromAny(m["cite_index"])
if !hasIdx {
return
}
if idx <= 0 {
idx = idx + 1
}
if idx <= 0 {
return
}
if existing, ok := c.explicit[idx]; ok && strings.TrimSpace(existing) != "" {
return
}
c.explicit[idx] = url
}
func (c *citationLinkCollector) addOrdered(url string) {
if _, ok := c.seen[url]; ok {
return
}
c.seen[url] = struct{}{}
c.ordered = append(c.ordered, url)
}
func citationIndexFromAny(v any) (int, bool) {
switch x := v.(type) {
case int:
return x, true
case int32:
return int(x), true
case int64:
return int(x), true
case float32:
return int(x), true
case float64:
return int(x), true
case string:
s := strings.TrimSpace(x)
if s == "" {
return 0, false
}
n, err := strconv.Atoi(s)
if err != nil {
return 0, false
}
return n, true
default:
return 0, false
}
}
func isWebURL(v string) bool {
v = strings.ToLower(strings.TrimSpace(v))
return strings.HasPrefix(v, "http://") || strings.HasPrefix(v, "https://")
}
func asString(v any) string {
s, _ := v.(string)
return s
}

View File

@@ -13,6 +13,7 @@ type CollectResult struct {
Text string
Thinking string
ContentFilter bool
CitationLinks map[int]string
}
// CollectStream fully consumes a DeepSeek SSE response and separates
@@ -28,11 +29,15 @@ func CollectStream(resp *http.Response, thinkingEnabled bool, closeBody bool) Co
text := strings.Builder{}
thinking := strings.Builder{}
contentFilter := false
collector := newCitationLinkCollector()
currentType := "text"
if thinkingEnabled {
currentType = "thinking"
}
_ = deepseek.ScanSSELines(resp, func(line []byte) bool {
if chunk, done, parsed := ParseDeepSeekSSELine(line); parsed && !done {
collector.ingestChunk(chunk)
}
result := ParseDeepSeekContentLine(line, thinkingEnabled, currentType)
currentType = result.NextType
if !result.Parsed {
@@ -59,5 +64,6 @@ func CollectStream(resp *http.Response, thinkingEnabled bool, closeBody bool) Co
Text: text.String(),
Thinking: thinking.String(),
ContentFilter: contentFilter,
CitationLinks: collector.build(),
}
}

View File

@@ -115,6 +115,22 @@ func TestCollectStreamWithCitation(t *testing.T) {
}
}
func TestCollectStreamExtractsCitationLinks(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/fragments/-1/results\",\"v\":[{\"url\":\"https://example.com/a\",\"cite_index\":0},{\"url\":\"https://example.com/b\",\"cite_index\":1}]}\n" +
"data: {\"p\":\"response/content\",\"v\":\"结论[citation:1][citation:2]\"}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, false, false)
if got := result.CitationLinks[1]; got != "https://example.com/a" {
t.Fatalf("expected citation 1 link, got %q", got)
}
if got := result.CitationLinks[2]; got != "https://example.com/b" {
t.Fatalf("expected citation 2 link, got %q", got)
}
}
func TestCollectStreamMultipleThinkingChunks(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/thinking_content\",\"v\":\"part1\"}\n" +