Skip to content

feat: search citations — 信源透传 + 结构化字段 + url_citation annotations#468

Merged
chenyme merged 3 commits intochenyme:mainfrom
Huan-zhaojun:pr/feat/search-sources
Apr 19, 2026
Merged

feat: search citations — 信源透传 + 结构化字段 + url_citation annotations#468
chenyme merged 3 commits intochenyme:mainfrom
Huan-zhaojun:pr/feat/search-sources

Conversation

@Huan-zhaojun
Copy link
Copy Markdown
Contributor

@Huan-zhaojun Huan-zhaojun commented Apr 13, 2026

Why

Grok 搜索时 SSE 流返回 44~400 条信源(web + X 帖子),但此前 grok2api 全部丢弃,
下游消费者(如 GrokSearch MCP)的 sources_count 为 0;
模型正文内联引用 [[N]](url) 也仅有裸 Markdown,缺少标准化的引用元数据。

本 PR 建立完整的搜索引用链路:信源透传 → 结构化字段 → 引用标注,三层覆盖全部 3 个 API 端点。

Summary

层级 功能 输出位置 开关
正文透传 ## Sources 段落(web + X 帖子 URL 列表) 响应正文末尾 show_search_sources(默认关)
结构化字段 search_sources: [{url, title, type}] 响应根对象 / message item 始终输出(无开关)
引用标注 url_citation annotations(URL、title、文本偏移) OpenAI 标准 annotations 字段 始终输出(有引用即产出)

Changes

采集层 — xai_chat.py StreamAdapter

信源采集:

  • webSearchResults: 原始 url + title,多帧累积去重
  • xSearchResults: postId + username 拼接 URL,text 前 50 字构造 title,空白归一化
  • 共享 _web_search_urls_seen set 跨类型去重
  • references_suffix() 统一转义 Markdown 特殊字符
  • search_sources_list() 返回 [{url, title, type}](type: "web" / "x_post"

引用标注:

  • _clean_token() 改为返回 (cleaned_text, local_annotations)
  • 新增 _pending_citations / _annotations / _text_offset 三态追踪
  • _render_replace 生成引用时同步记录元数据,per-token 精确定位
  • title 三级 fallback: card → webSearchResults → URL 本身
  • FrameEvent 新增 annotation_data 字段,annotations_list() 扁平输出

注入层 — 三端点 × 流式/非流式

Chat Completions (chat.py + _format.py):

  • 非流式: message.annotations(url_citation 格式)
  • 流式: final chunk delta.annotations
  • search_sources 置于响应根对象

Responses API (responses.py):

  • 流式: annotation.added 实时事件 + content_part.done / output_item.done 的 annotations 数组
  • 非流式: output_text 的 annotations 数组
  • search_sources 置于 message item 级别

Anthropic Messages (messages.py):

  • 非流式: TextBlock.annotations
  • 流式: message_delta.delta.annotations(自定义扩展,OpenAI 扁平格式)
  • search_sources 置于响应根对象 / message_delta.delta

设计说明: Anthropic 标准 citations 需 encrypted_index(专有加密索引)+ cited_text(源网页原文),
二者 Grok 均无法提供,故改用 OpenAI url_citation 扁平格式作自定义扩展。

多轮剥离 — chat.py _extract_message

  • 标记行 [grok2api-sources]: #(CommonMark link reference definition,渲染器不显示)
  • 正则覆盖 string content + block list content,CRLF 兼容
  • 仅匹配含标记行的段落,用户自写 ## Sources 不受影响

配置 & UI

  • config.defaults.toml 新增 show_search_sources = false(仅控制正文透传)
  • 管理面板新增开关 + 6 语言 i18n(zh/en/de/es/fr/ja)

Test plan

  • 开启 show_search_sources,发送搜索类问题 → 响应末尾出现 ## Sources 段落
  • 关闭该配置 → 无 ## Sources 正文,但 search_sources 字段仍存在
  • 多轮对话:第二轮请求中确认前轮 Sources 被剥离(不出现在 Grok 请求中)
  • 验证 search_sources 字段结构: [{url, title, type}],type 为 "web""x_post"
  • 验证 url_citation annotations: 包含 url、title、start_index、end_index
  • 非搜索查询(纯聊天)→ 无 Sources、无 search_sources、无 annotations
  • X 帖子 URL 格式: https://x.com/{username}/status/{postId}
  • CherryStudio Responses API 模式下底部渲染引用卡片
  • 三端点均覆盖:Chat Completions / Responses API / Anthropic Messages
  • 流式 & 非流式 & tool_calls 路径均正常输出

… Sources

Grok SSE 流中的 webSearchResults 和 xSearchResults 透传给下游消费者。

采集层(StreamAdapter):
  - webSearchResults: 直接使用原始 url + title
  - xSearchResults: postId+username 拼接 URL,text 前 50 字构造 title,
    空白归一化,共享 set 跨类型去重
  - references_suffix() 统一转义 Markdown 特殊字符后输出

多轮剥离(_extract_message):
  - 标记行 [grok2api-sources]: # (CommonMark link ref def,渲染器不显示)
  - 正则覆盖 string content + block list content,CRLF 兼容
  - 仅匹配含标记行的段落,用户自写 ## Sources 不受影响

配置:
  - features.show_search_sources(默认 false),管理面板可开关
  - 管理面板 + 6 语言 i18n (zh/en/de/es/fr/ja)
…ype}]

  搜索信源以 search_sources 结构化字段始终输出。
  show_search_sources 布尔开关仅控制是否同时追加 ## Sources 正文。

  采集层(StreamAdapter):
    - feed() 采集时标记 type: "web" / "x_post"
    - 新增 search_sources_list() 返回 [{url, title, type}] 或 None

  注入层(3 API × 流式/非流式 + tool_calls 路径):
    - Chat Completions: 响应根对象(避免 Vercel AI SDK strict schema)
    - Responses API: message item 级别
    - Anthropic Messages: 响应根对象 / message_delta.delta
    - tool_calls/tool_use 路径同步覆盖

  配置/前端:
    - config.defaults.toml 注释更新
    - config.html + 6 语言 i18n 描述更新

(cherry picked from commit ced0fe1)
搜索内联引用 [[N]](url) 同步输出 OpenAI 标准 url_citation annotations(URL、title、文本位置)。CherryStudio 走 Responses API 可渲染底部引用卡片。

采集层(StreamAdapter):
  - _clean_token() 改为返回 (cleaned_text, local_annotations)
  - 新增 _pending_citations / _annotations / _text_offset 三态
  - _render_replace 生成引用时同步记录元数据,per-token 定位
  - title 三级 fallback: card → webSearchResults → URL 本身
  - FrameEvent 新增 annotation_data 字段,annotations_list() 扁平输出

注入层(三端点 × 流式/非流式):
  - Responses API: 流式 annotation.added 实时事件 + content_part.done / output_item.done 的 annotations 数组
  - Chat Completions: 非流式 message.annotations;流式 final chunk 的 delta.annotations(嵌套 url_citation 格式)
  - Anthropic Messages: 非流式 TextBlock.annotations;流式 message_delta.delta.annotations(自定义扩展)

设计说明:
  - Anthropic 标准 citations 需 encrypted_index(专有加密索引,无法生成)+ cited_text(源网页原文,Grok 不提供),改用 OpenAI 扁平格式作自定义扩展
  - Chat Completions 流式放 delta.annotations 而非 choice.annotations:Vercel AI SDK schema 对 delta.annotations 有精确定义,必须放标准位置
  - per-token 定位(vs 流末全文扫):精确字符串匹配避免 URL 含 ) 截断、UTF-16 偏差影响可控、title 从 card 直取、annotation.added 事件自然可发

(cherry picked from commit 0fbb89c)
@Huan-zhaojun Huan-zhaojun changed the title feat: 信源透传 (webSearchResults + xSearchResults) feat: search citations — 信源透传 + 结构化字段 + url_citation annotations Apr 18, 2026
@chenyme chenyme merged commit 9ae5d19 into chenyme:main Apr 19, 2026
5 of 6 checks passed
@Huan-zhaojun Huan-zhaojun deleted the pr/feat/search-sources branch April 19, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants