diff --git a/internal/sse/citation_links.go b/internal/sse/citation_links.go index 978d02d..e20314c 100644 --- a/internal/sse/citation_links.go +++ b/internal/sse/citation_links.go @@ -66,7 +66,11 @@ func (c *citationLinkCollector) captureURLAndIndex(m map[string]any) { if !hasIdx { return } - if idx <= 0 { + // DeepSeek citation indices in search results are zero-based (0,1,2,...), + // while visible markers are one-based ([citation:1], [citation:2], ...). + // Normalize all non-negative explicit indices to one-based to avoid + // misalignment when 3+ citations are present. + if idx >= 0 { idx = idx + 1 } if idx <= 0 { diff --git a/internal/sse/consumer_edge_test.go b/internal/sse/consumer_edge_test.go index d0da4bf..700e193 100644 --- a/internal/sse/consumer_edge_test.go +++ b/internal/sse/consumer_edge_test.go @@ -131,6 +131,25 @@ func TestCollectStreamExtractsCitationLinks(t *testing.T) { } } +func TestCollectStreamExtractsCitationLinksForSequentialZeroBasedIndices(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},{\"url\":\"https://example.com/c\",\"cite_index\":2}]}\n" + + "data: {\"p\":\"response/content\",\"v\":\"结论[citation:1][citation:2][citation:3]\"}\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) + } + if got := result.CitationLinks[3]; got != "https://example.com/c" { + t.Fatalf("expected citation 3 link, got %q", got) + } +} + func TestCollectStreamMultipleThinkingChunks(t *testing.T) { resp := makeHTTPResponse( "data: {\"p\":\"response/thinking_content\",\"v\":\"part1\"}\n" +