☰
一次审批门翻转:opencodex 让 Cursor 适配器的模型重新用上 Web 搜索
2026/10/31 1:29:28 网站建设 项目流程

一次审批门翻转:opencodex 让 Cursor 适配器的模型重新用上 Web 搜索

【免费下载链接】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

opencodex 这个 provider 代理的 Cursor 适配器,把三个 Web 搜索审批查询的应答从 rejected 翻转为 approved,模型在 Cursor 路径上的原生 Web 搜索随之恢复。

先看结果

改造之后,经由 Cursor 适配器路径服务的模型(比如cursor/grok-4.5)可以发起原生联网检索了:搜索由 Cursor 服务端执行,结果注入模型上下文,答案照常以textDelta帧流回客户端。之前用户日志里那个web_search -> non-interactive bridge reject的死门,变成了真正搜得到内容的联网回合。🔓

它为什么之前卡住

opencodex 的 Cursor 适配器 负责把上游模型请求按 Cursor agent 协议转发给 Cursor 服务端。

关键在于协议里的一个细节:模型想联网之前,Cursor 服务端会先向客户端发一个interactionQuery权限审批请求——"我想执行webSearchRequestQuery,准吗?"客户端不答或拒绝,模型就放弃搜索。

opencodex 的桥接层是个无头客户端,没有真人坐在回合中间点按钮。之前的默认策略是所有交互查询一律回rejected,统一理由:"opencodex bridge is non-interactive; proceed without this interaction." 类比一下:门卫的原则是"谁要出门都不放行",模型伸手要搜索工具,桥接层在门口把它拦下了。搜索能力不是模型不会用,是被这一层挡死的。

开关是怎么翻的:空批准就是把搜索委托出去

Cursor 的审批协议是个二态开关。看生成的 protobuf 定义 agent_pb.ts:WebSearchRequestResponse的result是个oneof,只有两个分支——Approved和Rejected。

有意思的是Approved分支是个空消息:不带reason,不带任何参数,也没有"结果"字段可填。所以回一个空批准,语义上就等于"授权加委托"——搜索本体不在客户端执行,而是交给 Cursor 服务端去跑,服务端把结果注入模型上下文,模型的回答再流回来。

批准后服务端还会发来web_search_tool_call/exa_*_tool_call这类原生工具帧。它们只是展示层帧,protobuf-events.ts 里的mcpArgsFromToolCall()只对 opencodex 桥接的 Responses 工具返回参数,live-transport.ts 里的isClientToolFrame()也只认自己桥接的工具帧,原生工具帧会被事件映射器安全忽略——流不会停滞,也不会凭空冒出"不完整的工具调用"。✅

改动明细

  • 导入区(live-transport.ts):新增WebSearchRequestResponse_ApprovedSchema、ExaSearchRequestResponse_ApprovedSchema、ExaFetchRequestResponse_ApprovedSchema三个导入;移除翻转后不再使用的三个*RequestResponse_RejectedSchema;保留SwitchModeRequestResponse_RejectedSchema(switchMode 仍拒绝)。
  • 分支翻转:live-transport.ts 中planInteractionQueryReply()的webSearchRequestQuery/exaSearchRequestQuery/exaFetchRequestQuery三个分支,内部result从 rejected 换成空批准消息,replyCase标记同步改为*RequestResponse:approved:
// 翻转前:result: { case: "rejected", value: create(WebSearchRequestResponse_RejectedSchema, { reason: NON_INTERACTIVE_REASON }) } // 翻转后:result: { case: "approved", value: create(WebSearchRequestResponse_ApprovedSchema, {}) } // replyCase: "webSearchRequestResponse:approved"
  • 不动的部分:askQuestion、switchMode继续拒绝(回合中间没人应答,代理必须自主推进);createPlan回 success,plan 文本透传给 Codex 侧可见;setupVmEnvironment与未知查询回空应答加unsupported标记——宁可空答也不能抛错,抛错会把整条 gRPC 连接拖死。

📝 全部改动只落在src/adapters/cursor/live-transport.ts一个源文件加一个测试文件。

怎么确认它生效了:翻转之后怎么写测试

原来测试用一个参数化断言矩阵,一把梭断言 switchMode + web + exa 全部rejected。翻转之后,一个矩阵装不下两种结论,于是拆开:

  • switchMode 单列一个 case,断言结果仍是rejected;
  • web 和 exa 三兄弟一组,断言result.case === "approved"、replyCase带:approved后缀、id原样回传。

断言只读响应对象的结构,不需要导入三个 Approved schema。同一测试文件还守着其余分支的回归断言:createPlan回显 plan 文本、askQuestion被拒、空应答与unsupported标记、原生readToolCall不算客户端工具活动等,保证翻转没有波及其他交互路径。

验证命令:

bun test tests/providers/cursor/cursor-*.test.ts bun x tsc --noEmit

第一条跑 Cursor 适配器的全部测试,第二条确认类型干净。

代价与边界

批准不是免费的:搜索真的发生在 Cursor 服务端,会消耗用户 Cursor 账户的 web-search/Exa 配额,这部分开销算在用户头上,是这次改动必须认账的代价。⚖️

还有两条路径要分清:opencodex 自带合成 web_search sidecar(server/responses.ts的planWebSearch+runWithWebSearch),那是代理侧的另一条搜索路径,只在客户端请求里带 hostedweb_search工具时触发;这次改的是模型在 Cursor 适配器路径上使用Cursor 原生 Web 搜索的场景。两条路径正交,互不覆盖,也不会让原生搜索"顶掉"代理侧合成搜索。

收尾

这次改动刻意保持最小:一个纯函数决策点、一个测试文件、两个导入清单,不碰其他适配器、配置开关和版本号,把审批门每个分支的语义都用显式测试钉住。后续要给搜索加开关或额度控制,也是在同一个函数里加分支的事。

【免费下载链接】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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询