news 2026/9/23 1:26:54

opencodex 原生 Sidecar Web 搜索引用打通:将 url_citation 注解从 web_search_call_end 送达 Codex 桌面端 Sources 芯片

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
opencodex 原生 Sidecar Web 搜索引用打通:将 url_citation 注解从 web_search_call_end 送达 Codex 桌面端 Sources 芯片

【免费下载链接】opencodex

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

项目地址:https://gitcode.com/gh_mirrors/ope/opencodex
点击查看免费下载

导读

本文讲解 opencodex(Universal provider proxy for OpenAI Codex & Claude Code)在「原生 sidecar 对齐(native-sidecar parity)」阶段完成的一项关键协议改造:把 Web 搜索产生的来源引用(sources/citations)以 OpenAI Responses 标准的output_text.annotations形态转发给 Codex 客户端。读完本文,你将掌握该能力的前因后果、url_citation的线格式、从src/web-search/loop.tssrc/bridge/*的完整数据链路、流式与非流式两条路径的落地方式,以及去重、清空与安全过滤等细节,并能对照仓库源码与测试用例自行验证。

本文对应仓库文档:devlog/_fin/260630_native-sidecar-parity/40_phase3_websearch-sources.md,属于「260630 原生 sidecar 对齐」系列的第 3 阶段成果。

背景:问题与决策

问题现象

Codex 桌面应用(desktop app)在助手消息(assistant message)中执行过 Web 搜索后,会渲染一个Sources 芯片(chip)以及内联引用(inline citations)。而当时的 opencodex 代理已经能做到:

  • 从 sidecar 解析出url_citation并存入outcome.sources(携带 url + title);
  • 将这些来源通过formatWebSearchResults拼进 toolResult 的文本中,供模型在生成回答时引用。

但问题在于:最终助手消息始终输出output_text.annotations: [],即注解数组为空,因此 GUI 永远收不到引用信息,Sources 芯片自然无法渲染。

决策(用户拍板)

决策为:归一化到 APP 的线格式(wire shape)——即用output_text.annotations[]承载url_citation条目。这样做的原因与影响:

  • 对 codex-rs(TUI)无影响:TUI 目前忽略 annotations,该改动是纯增量(additive)的;
  • 对桌面应用有收益:桌面应用读取 annotations 来绘制 Sources 芯片。

从源码看,这一决策的落地痕迹非常清晰:src/types/request.tsOcxUrlCitation的注释直接写明「Surfaced on the search-end event and rendered by the bridge as aurl_citationannotation on the following assistant message (the desktop app's Sources chip reads these; the TUI ignores annotations, so this is additive)」,见 src/types/request.ts#L380-L388。

线格式:OpenAI Responses 标准注解

改造后的目标是让最终消息呈现如下标准结构(原文档中的 wire shape 示例):

{ "type": "output_text", "text": "...answer...", "annotations": [ { "type": "url_citation", "url": "https://...", "title": "Node.js Releases", "start_index": 0, "end_index": 0 } ] }

关键字段说明:

字段含义本实现中的取值
type注解类型固定为"url_citation"
url来源链接sidecar 返回的实际 URL
title来源标题可选;sidecar 有则带,无则省略
start_index/end_index引用在正文中的字符区间固定为0/0(见下文 OUT of scope)

变更地图(IN scope):四条改动路径

原文档将改动范围划为 4 项,逐一对应到仓库源码如下。

1. 类型定义:OcxUrlCitation与搜索结束事件

在 src/types/request.ts#L385-L388 新增接口:

export interface OcxUrlCitation { url: string; title?: string; }

它被携带在搜索结束事件上:web_search_call_end.sources?: OcxUrlCitation[]。也就是说,搜索结束事件现在可选地携带sources数组,为空时连字段都不出现(保持向后兼容)。

2. 搜索循环:批量去重并挂载 sources

在 src/web-search/loop.ts 的runSearchCall中,完成批量(batch)查询结果的来源收集与去重:

const sources: { url: string; title?: string }[] = []; const seenSrc = new Set<string>(); for (const r of results) { for (const s of r.outcome.sources) { if (seenSrc.has(s.url)) continue; seenSrc.add(s.url); sources.push(s.title ? { url: s.url, title: s.title } : { url: s.url }); } } yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: anySuccess ? "completed" : "failed", ...(sources.length > 0 ? { sources } : {}), };

见 src/web-search/loop.ts#L807-L821。要点:

  • 按 URL 去重:同一批次内多条查询可能命中同一链接,只保留一条;
  • title 可选:有 title 带 title,没有则只带 url;
  • 无来源不出字段sources为空时不输出该属性,避免污染事件结构;
  • status依「是否存在成功结果」取completedfailedqueries保留全部尝试过的查询以便 Codex 渲染原生复数标签。

3. 流式路径:bridgeToResponsesSSE 挂载注解

在流式桥接 src/bridge/sse.ts 中维护pendingWebSources缓冲:

let pendingWebSources: { url: string; title?: string }[] = [];

当收到web_search_call_end事件时通过appendSafeWebSearchSource(pendingWebSources, source)逐条累积(带安全过滤,见下文);当下一条助手消息闭合时,将其取出并映射为 annotations:

const anns = pendingWebSources.map(s => ({ type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0, }));

随后同时通过content_part.doneoutput_item.done两条 SSE 事件下发(分别对应part: { type: "output_text", text, annotations }content: [{ type: "output_text", text, annotations }],见 src/bridge/sse.ts#L443-L499),并在取走后立即清空缓冲,确保来源只绑定到恰好一条消息。

4. 非流式路径:buildResponseJSON 挂载注解

非流式路径同样维护pendingWebSources(src/bridge/response-json.ts#L195-L196),在flushText()中挂载并清空:

const annotations = pendingWebSources.map(s => ({ type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0, })); pendingWebSources = []; content: [{ type: "output_text", text, annotations }],

见 src/bridge/response-json.ts#L221-L228。同时该路径还包含一个容量保护:sourceBytes统计了待挂载来源的 JSON 字节数(src/bridge/response-json.ts#L221、L546-L548),配合安全过滤共同约束进入消息的注解规模。

OUT of scope:明确不做的事

原文档明确划出两项范围外工作,仓库实现也确实未涉及:

  1. 不做内联字符区间引用start_index/end_index本应指向正文中的具体字符位置,本实现固定发射0/0;桌面应用通过 url/title 绘制 Sources 芯片,不需要精确区间。同时 src/web-search/parse.ts 在解析侧本就丢弃 start/end 索引,只保留 url 与 title。
  2. 不改 toolResult 文本格式:模型仍然在文本内收到来源(formatWebSearchResults保持原样),只是消息注解层新增了结构化的引用。

数据链路全景

综合上述改动,一次带 Web 搜索的对话在 opencodex 代理内的完整引用链路为:

  1. sidecar 返回结果:解析侧(src/web-search/parse.ts)从完成态的 Responsesoutput[]数组与流式注解事件中提取来源——支持注解形态(response.output_text.annotation.added携带url_citation)与正文尾部Sources:区块两种形态;
  2. search call 事件:src/web-search/loop.ts 的runSearchCall产出web_search_call_beginweb_search_call_end,后者携带去重后的sources
  3. bridge 缓冲src/bridge/sse.ts(流式)与src/bridge/response-json.ts(非流式)分别把 sources 累积进pendingWebSources
  4. 消息闭合挂载:下一条助手消息闭合时,把缓冲映射为url_citationannotations,通过content_part.done/output_item.done(流式)或最终 JSON 的output[].message.content(非流式)下发;
  5. 清空缓冲:挂载后立即清空,保证引用只绑定到恰好一条消息。

安全与健壮性细节

引用来自外部搜索后端,属于不可信输入,仓库实现了多层防护:

  • 协议安全过滤appendSafeWebSearchSource会拒绝不安全的 URL(如javascript:alert(1))、含凭证的 URL(如https://user:pass@...)、含控制字符的路径,以及含非法控制字符的 title;
  • 去重:同一 URL 只保留首次出现的 title;
  • 数量与字节上限:注解数量与 JSON 字节数均受约束,防止单条消息被大量来源撑爆(见 src/bridge/response-json.ts#L221 的sourceBytes统计)。

对应测试用例 tests/adapters/bridge.test.ts#L1200-L1210 明确验证了「unsafe and oversized search sources are absent from cells and annotations」——恶意与超限来源既不会进入搜索 cell,也不会进入注解。

验收标准与测试印证

原文档给出的验收标准在仓库测试中均有对应覆盖:

验收 1:真实搜索后,流式与非流式路径的output_text.annotations均包含每个唯一来源的url_citation

  • 流式路径:tests/adapters/bridge.test.ts#L1161-L1180「streaming: web_search_call_end sources attach as url_citation annotations on the next message」——重放web_search_call_beginweb_search_call_end(sources)text_deltadone,断言response.output_item.done的 message 首 part 的annotations等于[{ type: "url_citation", url: "https://nodejs.org", title: "Node.js", start_index: 0, end_index: 0 }]
  • 非流式路径:tests/adapters/bridge.test.ts#L1182-L1198 以buildResponseJSON验证同样的注解出现在最终output[].message.content[0].annotations
  • 端到端: tests/web-search/web-search.test.ts#L2050-L2097 用真实 sidecar 返回的url_citation注解(https://nodejs.org/en/about/previous-releases/ "Node.js Releases")验证注解到达助手消息;L2101 起 还覆盖了「注解为空 + 正文 Sources 区块」的真实场景——sidecar 通常省略注解而把来源列在正文尾部,此时解析侧从Sources:区块提取并仍能产出url_citation注解(如 Node.js Download page、Node.js release archive 两条)。

验收 2:无搜索(或搜索失败/为空且无来源)的回合保持annotations: [],不回归。

桥接层在无pendingWebSources时发射空注解数组:流式路径的takeWebAnnotations()在缓冲为空时返回[](src/bridge/sse.ts#L448-L456),非流式路径同样在flushText()中输出空数组。

验收 3:来源绑定到搜索后的第一条助手消息,随后缓冲清空。

pendingWebSources在挂载后立即置空(src/bridge/response-json.ts#L225 与 src/bridge/sse.ts#L449 的take语义),确保不会泄漏到后续消息。

批量去重专项测试:tests/web-search/web-search.test.ts#L2169-L2259「web-search batched sources -> url_citation annotations」验证了两条查询命中同一 URL 时只产出一条注解(https://shared.test/doc只出现一次),不同 URL 则各自保留(https://shared.test/uniqueA独立成条),与loop.tsseenSrc去重逻辑一一对应。

总结

第 3 阶段「Web-search sources/citations to GUI」为 opencodex 补齐了 Web 搜索引用到 GUI 的最后一公里:以output_text.annotations承载url_citation的标准线格式,让 Codex 桌面应用的 Sources 芯片得以渲染,同时通过「挂载后即清空」保证引用绑定语义精确、通过去重与安全过滤保证注解内容可信,且对忽略注解的 TUI 完全透明、对无搜索回合零回归。对于希望深入协议实现的读者,建议按 src/types/request.ts → src/web-search/loop.ts → src/web-search/parse.ts → src/bridge/sse.ts / src/bridge/response-json.ts 的顺序阅读,并以 tests/adapters/bridge.test.ts 与 tests/web-search/web-search.test.ts 中的注解相关用例作为行为契约。

【免费下载链接】opencodex

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

项目地址:https://gitcode.com/gh_mirrors/ope/opencodex
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 1:25:27

Linux驱动开机自动加载全解析:modprobe、设备树与initramfs实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/23 1:24:51

奇安信天擎终端安全管理:管理员运维、策略配置与卸载退管指南

简介&#xff1a;奇安信天擎终端安全管理系统管理员手册面向企业终端安全运维人员、系统管理员及安全初学者&#xff0c;提供从产品认知到部署落地的完整指导。手册围绕产品简介、主要功能、典型部署场景与部署形态四大模块展开&#xff0c;涵盖终端安全管理、恶意代码检测、网…

作者头像 李华
网站建设 2026/9/23 1:21:49

VASP与QE应力应变计算全解析:从DFT参数到Python拟合

简介&#xff1a;面向材料科学领域的DFT计算学习者&#xff0c;这份资料将第一性原理软件VASP与Quantum Espresso中的力学计算流程&#xff0c;封装成可直接运行的Python脚本&#xff0c;适合已有一定计算基础、希望自动化处理应力应变数据的用户。压缩包共16个文件&#xff0c…

作者头像 李华
网站建设 2026/9/23 1:21:44

银河麒麟V10下Qt5.14.2应用打包:linuxdeployqt保姆级实战指南

开头做国产化适配的兄弟们应该都有体会&#xff0c;在银河麒麟V10上开发Qt应用不是最难的&#xff0c;真正折磨人的是把程序交给现场工程师之后&#xff0c;对方一句"双击打不开"就能让你瞬间破防。依赖库缺失、平台插件找不到、权限不对、甚至解压路径带空格都能给你…

作者头像 李华