opencodex 原生 Sidecar Web 搜索引用打通:将 url_citation 注解从 web_search_call_end 送达 Codex 桌面端 Sources 芯片
【免费下载链接】opencodexUniversal 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点击查看免费下载导读本文讲解 opencodexUniversal 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.ts到src/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-rsTUI无影响TUI 目前忽略 annotations该改动是纯增量additive的对桌面应用有收益桌面应用读取 annotations 来绘制 Sources 芯片。从源码看这一决策的落地痕迹非常清晰src/types/request.ts中OcxUrlCitation的注释直接写明「Surfaced on the search-end event and rendered by the bridge as aurl_citationannotation on the following assistant message (the desktop apps 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_citationurl来源链接sidecar 返回的实际 URLtitle来源标题可选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 Setstring(); 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依「是否存在成功结果」取completed或failedqueries保留全部尝试过的查询以便 Codex 渲染原生复数标签。3. 流式路径bridgeToResponsesSSE 挂载注解在流式桥接 src/bridge/sse.ts 中维护pendingWebSources缓冲let pendingWebSources: { url: string; title?: string }[] [];当收到web_search_call_end事件时通过appendSafeWebSearchSource(pendingWebSources, source)逐条累积带安全过滤见下文当下一条助手消息闭合时将其取出并映射为 annotationsconst anns pendingWebSources.map(s ({ type: url_citation, url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0, }));随后同时通过content_part.done与output_item.done两条 SSE 事件下发分别对应part: { type: output_text, text, annotations }与content: [{ type: output_text, text, annotations }]见 src/bridge/sse.ts#L443-L499并在取走后立即清空缓冲确保来源只绑定到恰好一条消息。4. 非流式路径buildResponseJSON 挂载注解非流式路径同样维护pendingWebSourcessrc/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明确不做的事原文档明确划出两项范围外工作仓库实现也确实未涉及不做内联字符区间引用start_index/end_index本应指向正文中的具体字符位置本实现固定发射0/0桌面应用通过 url/title 绘制 Sources 芯片不需要精确区间。同时 src/web-search/parse.ts 在解析侧本就丢弃 start/end 索引只保留 url 与 title。不改 toolResult 文本格式模型仍然在文本内收到来源formatWebSearchResults保持原样只是消息注解层新增了结构化的引用。数据链路全景综合上述改动一次带 Web 搜索的对话在 opencodex 代理内的完整引用链路为sidecar 返回结果解析侧src/web-search/parse.ts从完成态的 Responsesoutput[]数组与流式注解事件中提取来源——支持注解形态response.output_text.annotation.added携带url_citation与正文尾部Sources:区块两种形态search call 事件src/web-search/loop.ts 的runSearchCall产出web_search_call_begin→web_search_call_end后者携带去重后的sourcesbridge 缓冲src/bridge/sse.ts流式与src/bridge/response-json.ts非流式分别把 sources 累积进pendingWebSources消息闭合挂载下一条助手消息闭合时把缓冲映射为url_citationannotations通过content_part.done/output_item.done流式或最终 JSON 的output[].message.content非流式下发清空缓冲挂载后立即清空保证引用只绑定到恰好一条消息。安全与健壮性细节引用来自外部搜索后端属于不可信输入仓库实现了多层防护协议安全过滤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_begin→web_search_call_end(sources)→text_delta→done断言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.ts中seenSrc去重逻辑一一对应。总结第 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 中的注解相关用例作为行为契约。赞分享【免费下载链接】opencodexUniversal 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点击查看免费下载相关推荐AgentsView Desktop 桌面端实战用 Tauri Sidecar 把 Go 后端打包进原生桌面应用AgentsView Desktop 桌面端实战用 Tauri Sidecar 把 Go 后端打包进原生桌面应用 本文基于仓库内 desktop/READMEAI 应用数据分析数据可视化可观测性终极指南5步将Web应用桌面化打造原生体验的桌面应用终极指南5步将Web应用桌面化打造原生体验的桌面应用 Nativefier是一款强大的工具能让你轻松将任何网页转变为桌面应用带来原生应用般的使用体验。无CLI桌面应用开发工具opencodex 的 Kiro 适配器实现解析从 AWS Eventstream 解码到 Codex CLI 端到端打通opencodex 的 Kiro 适配器实现解析从 AWS Eventstream 解码到 Codex CLI 端到端打通 本文以 opencodex 仓库创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考