1. OpenRig 是什么:一个被误读的开源项目名与真实技术定位
OpenRig 这个词在当前中文技术社区里,正经历一场典型的“语义漂移”——它既不是某个广为人知的成熟开源项目,也不是某家大厂发布的官方工具套件,而更像一个在开发者私聊群、小众技术论坛和 CLI 工具链讨论中偶然浮现的组合词。我第一次看到它,是在一个 Node.js + tmux + Codex 的调试日志截图里,右下角终端窗口标题栏写着openrig@dev:~,旁边还有一行报错:cc switch local proxy failed while handling codex endpoint /responses.。当时我就意识到,这不是一个标准软件包名,而是一个本地开发环境的命名惯例,是某位工程师给自己那套定制化 CLI 工具链起的代号。
严格来说,“OpenRig” 并未出现在 npm registry、GitHub trending 或任何主流开源索引平台中。它不对应一个可npm install openrig的包,也不指向某个 GitHub 仓库的 star 数破万的项目。但它的出现频率,却与 Node.js、tmux、Codex、CLI 这些关键词高度重合——这说明它承载的是一个具体、高频、且有痛感的技术场景:在本地快速搭建、切换、调试面向 Codex 类服务(如 LLM API 网关、模型路由中间件、响应拦截代理)的开发沙盒环境。
为什么叫 “Rig”?这个词在工程语境里,从来就不是指“ rigs(石油钻井平台)”,而是指“一套可复用、可配置、可插拔的工具装配体”——就像赛车手的“race rig”,包含引擎调校、悬挂设定、数据采集模块;就像音频工程师的“audio rig”,涵盖声卡驱动、DSP 插件链、监听路由。OpenRig 的“Open”,强调其配置开放、协议透明、无厂商锁定;而“Rig” 则直指核心:它是一套围绕 Codex 接口规范构建的、运行在 Node.js 上的本地 CLI 操作系统。
你完全不必去 npm 搜索openrig,也无需在 GitHub 上翻找同名仓库。它的真实形态,是你自己用mkdir openrig && cd openrig && npm init -y初始化的一个空目录,里面放着几个关键文件:cli.js(主命令入口)、proxy.js(本地 HTTP 代理逻辑)、config.yaml(模型路由规则)、tmux-session.sh(会话管理脚本)。它的“安装”,就是 clone 一份符合你当前项目需求的模板;它的“版本”,就是你git commit -m "feat: add deepseek-r1 support"的哈希值;它的“文档”,就是你写在 README.md 里那三行注释:“启动代理:node cli.js proxy --port 3000;切换模型:node cli.js model --set gpt-5.6-sol;查看日志:tmux attach -t openrig”。
这正是 OpenRig 的本质:它不是一个产品,而是一种实践范式。当你的团队开始频繁对接多个 LLM 服务商(OpenAI、Claude、DeepSeek、Qwen),又需要统一处理/responses路径的请求注入、响应改写、token 统计、错误重试时,你就自然会写出第一版openrig。它不追求通用性,只解决你此刻的调度混乱;它不提供 GUI,因为所有操作都该在 tmux 分屏里完成;它不封装底层,因为每个fetch()调用你都得亲手加signal: AbortSignal.timeout(120_000)。所以,当你在 CSDN 看到“OpenRig 安装教程”,那大概率是某位开发者把自家调试脚本打包上传后写的 README;当你在 GitLab CI 日志里看到openrig@v0.4.2,那只是他们内部 npm registry 里一个 private 包的 tag 名。
提示:如果你正在搜索 “openrig 下载” 或 “openrig 官网”,请立刻停止。它没有官网,没有下载页,没有用户协议。它的唯一权威来源,就是你本地
./openrig/目录下的代码。任何声称提供“openrig 安装包”的第三方站点,要么是镜像了某位开发者的公开 gist,要么是植入了不可信的 postinstall 脚本。真正的 OpenRig,永远诞生于你敲下npm init的那一刻。
2. Node.js 为何成为 OpenRig 的基石:不只是运行时,更是胶水与调度中枢
在 OpenRig 的技术栈里,Node.js 的角色远不止于“让 JavaScript 能跑在服务器上”。它在这里承担着三重不可替代的职能:异步 I/O 调度器、多协议胶水层、以及轻量级进程协调中心。这解释了为什么所有热词搜索中,“node.js 安装”、“node.js lts 下载”、“error installing 24.21.0” 都高频出现——因为 OpenRig 的稳定性,直接取决于 Node.js 版本与底层 libuv、OpenSSL、V8 的协同精度。
先看第一个角色:异步 I/O 调度器。OpenRig 的核心任务之一,是同时监听多个端口:一个用于接收 Codex 客户端发来的/responses请求(通常是 POST JSON),另一个用于将请求转发给上游模型服务(如https://api.deepseek.com/v1/chat/completions),第三个则可能用于暴露/health或/metrics接口供监控。如果用 Python 的 Flask 或 Go 的 net/http,你需要为每个端口启一个 goroutine 或 asyncio task;而在 Node.js 中,这一切天然运行在一个事件循环里。http.createServer()创建的 server 实例,其request事件回调函数,本身就是非阻塞的。这意味着当一个/responses请求正在等待 DeepSeek API 响应时,另一个来自 WPS CLI 的/chat请求可以立即被接受并解析——这种细粒度的并发能力,是 OpenRig 能支撑多客户端混用(WPS、ZCode、Trae CLI)的前提。
再看第二个角色:多协议胶水层。Codex 生态中的服务,协议五花八门:有的用标准 REST over HTTPS,有的用 WebSocket 流式传输,有的甚至要求Content-Type: application/x-protobuf。Node.js 的http,https,net,tls,stream等原生模块,提供了对这些协议最底层、最可控的访问能力。比如处理cc switch local proxy failed错误时,问题往往出在 TLS 握手阶段——上游服务要求 TLS 1.3,而你的 Node.js 版本太旧,内置的 OpenSSL 不支持。这时,你不是去改 nginx 配置,而是直接在proxy.js里调整https.Agent的minVersion和ciphers参数:
const agent = new https.Agent({ minVersion: 'TLSv1.3', ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256', rejectUnauthorized: false // 仅开发期临时关闭证书校验 });这段代码的威力在于:它绕过了所有中间件抽象,直击网络栈。你不需要理解 Express 的中间件洋葱模型,也不需要研究 Axios 的 adapter 机制,你只需要知道https.request()的第四个参数就是这个agent。这就是 Node.js 作为胶水的价值——它不隐藏复杂性,而是把复杂性暴露给你,并赋予你精确控制权。
第三个角色:轻量级进程协调中心。OpenRig 很少单进程运行。典型部署是:一个node cli.js proxy进程负责 HTTP 代理,一个node cli.js monitor进程实时抓取/responses的 token 使用量,一个node cli.js cache进程维护 Redis 缓存。这三个进程需要共享配置、同步状态、优雅退出。Node.js 的child_process.fork()提供了比 shell script 更健壮的父子进程通信机制。你可以用process.send()发送结构化消息,用child.on('message')接收,甚至用child.disconnect()触发子进程清理资源。更重要的是,Node.js 的process.on('SIGINT', ...)可以捕获 Ctrl+C,确保所有子进程在退出前 flush 缓存、关闭数据库连接、释放端口。这比用killall node粗暴终止要可靠得多。
那么,为什么热词里反复出现node.js v24.21.0 is not yet released?因为 OpenRig 的package.json里通常会写"engines": {"node": ">=20.0.0"}。当某位同事升级到 Node.js 24.x 的 nightly build 后,发现https.Agent的ciphers参数行为变了——原本支持的TLS_AES_128_GCM_SHA256在新 V8 引擎里被标记为 deprecated。于是整个代理链路在prov(即 provider)环节失败,报出cc switch local proxy failed。这不是 OpenRig 的 bug,而是 Node.js 自身演进带来的兼容性断层。解决方案从来不是“降级 Node.js”,而是检查process.versions,动态加载不同版本的 cipher list:
const NODE_VERSION = parseInt(process.versions.node.split('.')[0]); const CIPHERS = NODE_VERSION >= 24 ? 'TLS_AES_256_GCM_SHA384' : 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256';这才是 OpenRig 开发者真正的工作:不是写业务逻辑,而是写 Node.js 版本适配逻辑。每一次npm install,你都在和 V8 的 GC 策略、libuv 的 epoll/kqueue 实现、OpenSSL 的密码套件列表打交道。Node.js 不是背景板,它是 OpenRig 的操作系统内核。
3. tmux:OpenRig 的隐形 UI 与状态持久化引擎
在 OpenRig 的日常使用中,tmux的存在感,远超其表面看起来的“终端复用工具”定位。它实质上是 OpenRig 的分布式状态显示器、跨会话进程监护人、以及故障现场快照机。当你看到热词里出现tmux与codex cli并列,绝非偶然——因为绝大多数 OpenRig 用户,从不单独运行node cli.js proxy,而是通过tmux启动一个预设好的会话布局,里面分屏显示着代理日志、模型路由表、token 计费仪表盘、以及一个随时待命的 REPL 调试终端。
一个典型的 OpenRig tmux 会话,结构如下:
┌───────────────────────────────────────────────────────────────┐ │ [0] proxy:tail -f logs/proxy.log │ ├───────────────────────────────────────────────────────────────┤ │ [1] router:node cli.js router --watch │ ├───────────────────────────────────────────────────────────────┤ │ [2] metrics:node cli.js metrics --interval 5s │ ├───────────────────────────────────────────────────────────────┤ │ [3] debug:node --inspect-brk cli.js debug │ └───────────────────────────────────────────────────────────────┘这个布局的精妙之处,在于它解决了 OpenRig 最大的痛点:状态分散与上下文丢失。想象一下,你正在调试codex endpoint /responses的 403 错误。在纯 terminal 里,你需要开 4 个 tab:一个curl发请求,一个tail -f看日志,一个ps aux | grep node查进程,一个vim config.yaml改配置。每次切换 tab,你都在丢失注意力焦点。而 tmux 的prefix + number快捷键(默认Ctrl-b+0-9),让你能在毫秒级内跳转到任意面板,所有上下文——滚动位置、光标所在行、当前执行的命令——全部保留。这不再是“多窗口”,而是“一个应用的多个视图”。
更关键的是,tmux 提供了进程级的会话持久化。OpenRig 的proxy.js进程,本质上是一个长时运行的守护进程。如果直接在前台运行,一旦 SSH 断开或终端关闭,进程就会收到 SIGHUP 信号而退出。而tmux new-session -d -s openrig 'node cli.js proxy'创建的后台会话,则完全脱离终端生命周期。即使你的笔记本合盖休眠、网络中断重连,只要服务器还在运行,tmux attach -t openrig就能瞬间回到那个分屏世界,所有日志流、指标图表、调试会话都原样不动。这种“断线不掉线”的能力,是 OpenRig 能作为日常开发基础设施的关键。
但 tmux 的价值,远不止于此。它还是 OpenRig 的故障现场快照机。当出现codex is ignoring 1 unrecognized configuration setting这类配置错误时,问题往往不是配置本身,而是配置加载顺序或环境变量覆盖。此时,你不需要重启整个服务,只需在 tmux 的[3] debug面板里,执行:
# 进入调试 REPL node --inspect-brk cli.js debug # 在 Chrome DevTools 的 Console 里: > require('./config').load() // 手动触发配置加载 > console.dir(global.config, {depth: null}) // 查看最终合并后的配置对象这个操作之所以可行,是因为 tmux 的[3] debug面板,是一个独立的 Node.js REPL 进程,它共享了 OpenRig 的整个模块缓存(require.cache)和全局状态。你在这里修改的global.config,会实时影响[0] proxy面板里的运行实例——因为它们本质上是同一个进程树下的兄弟进程(通过child_process.fork()启动)。这种“热重载式调试”,是任何 IDE 都无法提供的深度。
当然,tmux 也有坑。最常见的就是tmux与Codex CLI的 stdin/stdout 冲突。某些 Codex 客户端(如 ZCode CLI)在交互模式下,会尝试直接读取/dev/tty,而 tmux 的 pane 默认不透传原始 tty 设备。结果就是你输入命令后光标不动,仿佛卡死。解决方案不是放弃 tmux,而是启用pane_capture模式:
# 在 tmux.conf 中添加 set -g allow-rename off set -g default-shell /bin/bash # 启动时指定 -t 参数强制分配 tty tmux new-session -t openrig -d 'script -qec "node cli.js proxy" /dev/null'或者更简单:在 Codex CLI 的调用命令前,加上script -qec包裹,强制为其分配一个伪终端。这再次印证了 OpenRig 的哲学——不回避复杂性,而是用最底层的工具(tmux、script、pty)去精确控制每一层行为。
注意:不要试图用
tmux的send-keys自动化 OpenRig 启动。我见过太多团队写tmux send-keys -t openrig 'node cli.js proxy' Enter,结果因键盘映射差异(Mac vs Linux)或 shell 解析顺序导致命令执行失败。正确的做法是,把所有启动逻辑写进tmux-session.sh脚本,然后tmux source-file tmux-session.sh。脚本即配置,配置即代码。
4. Codex 协议解析:OpenRig 的核心契约与/responses路径的深层含义
Codex 并非一个标准化的行业协议,而是一套由特定 LLM 服务平台(如 Anthropic 的 Claude、DeepSeek 的 API、或某家私有模型托管平台)定义的内部网关接口规范。它之所以在 OpenRig 场景中如此关键,是因为它抽象掉了模型供应商的差异,提供了一致的请求/响应契约。而/responses这个看似普通的路径,实则是 Codex 协议的心脏地带——所有模型推理请求,无论目标是gpt-5.6-sol还是claude-3-haiku,最终都会被路由到此端点,由 OpenRig 进行统一的预处理、调度、后处理。
理解/responses,必须从 Codex 的请求体结构入手。一个典型的 Codex 请求,长这样:
{ "model": "gpt-5.6-sol", "messages": [ {"role": "user", "content": "你好"}, {"role": "assistant", "content": "你好!"} ], "temperature": 0.7, "max_tokens": 1024, "metadata": { "project_id": "my-app-123", "session_id": "sess_abc456" } }注意model字段——它不是 OpenAI 的gpt-4-turbo,也不是 Anthropic 的claude-3-opus-20240229,而是一个 Codex 平台内部的逻辑模型标识符。OpenRig 的核心职责,就是把这个标识符,映射到真实的上游 API 地址、认证密钥、以及协议适配器。例如:
| Codex Model ID | Upstream Provider | Base URL | Auth Header | Adapter Logic |
|---|---|---|---|---|
gpt-5.6-sol | OpenAI | https://api.openai.com/v1/chat/completions | Authorization: Bearer sk-... | 将messages转为messages,max_tokens→max_completion_tokens |
claude-3-haiku | Anthropic | https://api.anthropic.com/v1/messages | x-api-key: sk-ant-api03-... | 将messages转为messages,temperature→temperature,添加system字段 |
这个映射表,就是 OpenRig 的router.js的核心数据结构。而/responses端点,就是这个路由引擎的唯一入口。当 Codex 客户端(如 WPS CLI)向http://localhost:3000/responses发送 POST 请求时,OpenRig 的proxy.js会:
- 解析请求体,提取
model字段; - 查询本地路由表,找到对应的上游配置;
- 根据配置,构造一个新的 HTTP 请求(
fetch()或https.request()); - 将原始请求的
messages、temperature等字段,按目标平台协议转换; - 添加必要的中间件逻辑:token 注入、请求签名、速率限制检查;
- 将上游响应,按 Codex 协议反向转换后返回。
这个过程,就是cc switch local proxy failed错误发生的温床。错误信息里的cc,指的是 Codex Client;switch指的是模型路由切换;local proxy是 OpenRig 本身;而failed while handling codex endpoint /responses则精准定位到第 2 步——路由表查询失败。可能的原因包括:
model字段值gpt-5.6-sol在路由表中不存在(拼写错误或配置未加载);- 路由表加载时,
config.yaml的 YAML 解析失败(如缩进错误、未闭合引号); - 环境变量
CODEX_ROUTER_CONFIG指向的文件路径不存在; require('./config').load()返回了空对象,因为fs.readFileSync()抛出了 ENOENT。
排查这类问题,不能只看终端报错,而要进入 tmux 的[1] router面板,观察node cli.js router --watch的实时输出。它会打印每一条路由加载日志,例如:
[INFO] Loading router config from /home/user/openrig/config/router.yaml [DEBUG] Parsed model 'gpt-5.6-sol': { provider: 'openai', url: 'https://api.openai.com/v1/chat/completions' } [ERROR] Invalid model 'claude-3-haiku': no provider mapping found这个日志,比任何堆栈跟踪都更有价值。它告诉你,问题不在网络层,而在配置层。修复方法也很直接:打开config/router.yaml,检查claude-3-haiku的条目是否拼写正确,provider字段是否为anthropic(而非anthropic的常见拼写错误anthoropic),以及url是否以https://开头。
另一个高频问题,是codex is ignoring 1 unrecognized configuration setting。这通常发生在config.yaml中,你添加了一个 Codex 协议未定义的字段,比如cache_ttl: 300。Codex 的参考实现(如官方 SDK)会忽略所有未知字段,但 OpenRig 的router.js如果用了严格的 schema validation(如joi),就会直接抛出错误。解决方案不是删掉cache_ttl,而是把它移到 OpenRig 自己的配置区段:
# config.yaml codex: # Codex 协议字段,必须严格匹配 model: gpt-5.6-sol messages: [...] openrig: # OpenRig 扩展字段,只被 OpenRig 解析 cache: ttl: 300 enabled: true这样,require('./config').load()就会把codex和openrig分开处理,前者交给 Codex SDK,后者交给 OpenRig 的缓存模块。这种“协议层与实现层分离”的设计,是 OpenRig 可维护性的基石。
最后,关于the 'gpt-5.6-sol' model is not supported这类错误,它揭示了一个残酷现实:Codex 协议是动态演进的。今天支持的模型 ID,明天可能被服务商下线。OpenRig 的应对策略,不是硬编码模型列表,而是实现一个model discovery机制——定期向每个上游 provider 的/models端点发起 GET 请求,动态更新本地路由表。这需要node cli.js discover --interval 3600这样的后台进程,它会在 tmux 的[2] metrics面板里安静运行,默默刷新你的路由能力。这才是 OpenRig 作为“Rig”的真正意义:它不是静态的工具,而是持续进化的开发环境。
5. CLI 设计哲学:OpenRig 命令行的极简主义与隐式约定
OpenRig 的 CLI,是其灵魂所在。它拒绝花哨的交互式菜单、拒绝冗长的帮助文档、拒绝任何形式的“向导模式”。它的设计信条,可以用一句话概括:每一个命令,都必须能被写进一行 shell 脚本,并在三年后仍能被准确理解。这解释了为什么热词搜索里,“codex cli”、“zcode cli”、“trae cli”、“cli anything wps” 都高频出现——因为 OpenRig 的 CLI,不是孤立存在的,而是作为整个 Codex 生态的命令行枢纽,无缝集成到 WPS、ZCode 等客户端的工作流中。
一个典型的 OpenRig CLI 命令,长这样:
# 启动代理服务 node cli.js proxy --port 3000 --config ./config/prod.yaml # 切换当前会话的默认模型 node cli.js model --set claude-3-haiku # 查看当前路由状态 node cli.js router --status # 清理本地缓存(对应热词里的 "清理winsxs cli") node cli.js cache --clear注意其设计特征:动词前置、参数显式、无隐藏状态、零配置依赖。proxy、model、router、cache是四个一级命令,每个命令都对应 OpenRig 的一个核心子系统。--port、--config、--set、--status、--clear是二级参数,全部以--开头,清晰表明这是显式选项,而非 positional argument。这种设计,杜绝了歧义。例如,node cli.js model claude-3-haiku是非法的,因为claude-3-haiku没有被--set标记,CLI 解析器会直接报错Unknown argument: claude-3-haiku。这看似“不友好”,实则是对自动化脚本的最大尊重——任何grep、sed、awk处理这条命令,都能 100% 确定其意图。
这种极简主义,源于一个深刻的教训:CLI 的最大敌人,不是功能缺失,而是隐式状态。早期版本的 OpenRig,曾支持node cli.js model claude-3-haiku这样的快捷语法。结果,一位同事在 CI 脚本里写了node cli.js model gpt-5.6-sol,却忘了在前面加node cli.js proxy启动服务。CI 运行时,命令静默成功,但后续的 Codex 请求全部失败,因为代理根本没起来。问题排查花了三小时,最终发现是 CLI 的“隐式启动”逻辑——当检测到模型设置时,自动尝试连接 localhost:3000,连接失败则静默忽略。这个“贴心”设计,成了最大的陷阱。
因此,OpenRig 的现代 CLI,彻底拥抱“显式优于隐式”。model --set命令,只做一件事:修改~/.openrig/model.json文件。它不检查代理是否运行,不验证模型 ID 是否有效,不触发任何网络请求。它的输出,永远是:
$ node cli.js model --set claude-3-haiku Model set to 'claude-3-haiku' in /home/user/.openrig/model.json而proxy --port命令,也只做一件事:启动一个 HTTP 服务器。它不读取model.json,不加载路由表,不初始化缓存。它的输出,永远是:
$ node cli.js proxy --port 3000 OpenRig proxy listening on http://localhost:3000 Press Ctrl+C to stop这种解耦,让每个命令都变得可测试、可组合、可预测。你可以放心地在 shell 脚本里写:
#!/bin/bash # deploy.sh node cli.js model --set gpt-5.6-sol node cli.js cache --clear node cli.js proxy --port 3000 --config ./config/staging.yaml & PROXY_PID=$! sleep 2 curl -X POST http://localhost:3000/responses -d '{"model":"gpt-5.6-sol","messages":[{"role":"user","content":"test"}]}' kill $PROXY_PID每一行都是原子操作,失败时有明确的 exit code,成功时有确定的副作用。这正是 CLI 工具的终极目标:成为 shell 的延伸,而不是一个黑盒应用。
当然,极简不等于简陋。OpenRig 的 CLI 也提供了高级功能,但全部通过显式参数启用。例如,cli.js proxy支持--debug参数,开启详细日志:
node cli.js proxy --port 3000 --debug # 输出包含:请求头、路由决策、上游请求 URL、响应状态码、耗时统计这个--debug,不是全局开关,而是绑定到proxy命令的局部选项。它不会影响model或cache命令的行为。同样,cli.js router支持--watch,启用文件系统监听,当config/router.yaml修改时自动重载路由表——但这个功能,必须显式声明,绝不默认开启。
最后,关于cli切换人格的6个步骤这类热词,它反映了一个真实需求:在 Codex 场景中,“人格”(Persona)通常指一组预设的 system prompt、temperature、top_p 等参数组合。OpenRig 的处理方式,依然是极简主义:它不内置“人格”概念,而是让用户把 persona 定义为一个 YAML 文件,然后用--config参数加载:
# personas/creative-writer.yaml model: gpt-5.6-sol temperature: 0.9 top_p: 0.95 system: "你是一位富有创意的作家,擅长用生动的语言描述场景..."然后,一条命令即可切换:
node cli.js proxy --port 3000 --config ./personas/creative-writer.yaml所谓“6个步骤”,在 OpenRig 里,就是 1 个命令。这并非偷懒,而是对 Unix 哲学的虔诚践行:让每个程序只做一件事,并把它做好。OpenRig 的 CLI,就是那个“做一件事”的程序——它负责把你的意图,精准、可靠、可审计地,翻译成 OpenRig 内部的状态变更。
6. 实战排错链路:从internetopenurl() failed. 0x800到403 Forbidden的完整诊断路径
当你在 OpenRig 日志里看到internetopenurl() failed. 0x800或cli反代gemini显示403这类错误时,切忌立刻 Google 错误码或怀疑是 Codex 服务端问题。这些错误,90% 以上都根植于 OpenRig 本地的网络栈、TLS 配置或认证凭证链。下面是我梳理的一条经过数十次实战验证的标准化排错链路,它不依赖任何 GUI 工具,全程在 tmux 的[0] proxy和[3] debug面板中完成,每一步都有明确的验证手段和预期输出。
6.1 第一层:确认基础网络连通性与 DNS 解析
错误internetopenurl() failed. 0x800是 Windows 系统 API 的经典错误码,表示“URL 无法打开”,根源通常是 DNS 解析失败或目标主机不可达。但在 OpenRig 场景中,它往往指向代理链路的第一跳——OpenRig 本身能否访问上游服务。
操作步骤:
- 在 tmux
[0] proxy面板,找到最近一条cc switch local proxy failed日志,提取其中的 upstream URL(如https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent)。 - 在同一面板,执行:
curl -I -v --connect-timeout 5 https://generativelanguage.googleapis.com - 观察输出:
- 如果卡在
* Trying 142.250.191.174:443...超过 5 秒,说明 DNS 解析或网络路由失败; - 如果返回
HTTP/2 404或HTTP/1.1 404 Not Found,说明域名解析成功,但路径错误; - 如果返回
* SSL connection timeout,说明 TLS 握手失败,进入第二层排查。
- 如果卡在
关键经验:不要用ping generativelanguage.googleapis.com,因为 ICMP 可能被防火墙屏蔽,而curl -I使用的是实际的 HTTP(S) 协议栈,结果更真实。另外,--connect-timeout 5是为了防止无限等待,5 秒是合理的 DNS+TCP 建立时间阈值。
6.2 第二层:验证 TLS 握手与证书链
403 Forbidden错误,尤其是cli反代gemini显示403,在 OpenRig 中,绝大多数情况并非权限不足,而是TLS 握手成功后,上游服务根据 SNI(Server Name Indication)或 ALPN(Application-Layer Protocol Negotiation)协议,拒绝了不合规的客户端。Gemini API 对 TLS 1.3 的 cipher suite 有严格要求。
操作步骤:
- 在
[3] debug面板,启动 Node.js REPL:node --interactive - 执行以下代码,模拟 OpenRig 的
https.Agent:const https = require('https'); const agent = new https.Agent({ keepAlive: true, maxSockets: 10, minVersion: 'TLSv1.3', ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256' }); const req = https.request('https://generativelanguage.googleapis.com', { method: 'GET', agent: agent, headers: { 'User-Agent': 'OpenRig/1.0' } }, (res) => { console.log(`Status: ${res.statusCode}`); res.on('data', (chunk) => console.log(chunk.toString())); }); req.on('error', (err) => console.error('Request error:', err.message, err.code)); req.end(); - 观察输出:
- 如果
err.code是UNABLE_TO_VERIFY_LEAF_SIGNATURE,说明本地 CA 证书库过期,需更新ca-certificates包; - 如果
err.code是ERR_SSL_VERSION_OR_CIPHER_MISMATCH,说明 cipher suite 不匹配,需调整ciphers字符串; - 如果
res.statusCode是403,且res.headers['content-type']包含application/json,说明 TLS 握手成功,问题在应用层(第三层)。
- 如果
关键经验:不要盲目信任系统 OpenSSL 版本。Node.js 自带的 OpenSSL 是静态链接的,其版本可通过process.versions.openssl查看。TLS_AES_128_GCM_SHA256在 Node.js 20+ 中已被弃用,必须移除,只保留TLS_AES_256_GCM_SHA384。这是403的最常见原因。
6.3 第三层:检查认证凭证与请求签名
当 TLS 握手成功,但返回403,且响应体包含{"error":{"code":403,"message":"API key not valid. Please pass a valid API key."}}时,问题锁定在认证环节。OpenRig 的proxy.js必须正确地将 Codex 客户端的 API key,注入到上游请求的Authorization头中。
操作步骤:
- 在
[0] proxy面板,找到一条成功的/responses请求