
1. OpenRig 是什么一个被误读的开源工具链命名冲突现场“OpenRig”这个词最近在开发者社区里频繁闪现但几乎没人能说清它到底指什么。你搜“openrig”首页跳出来的不是项目官网而是大量混杂着 **Node.js 安装失败日志、tmux 会话崩溃截图、Codex CLI 报错堆栈、以及一连串cc switch local proxy failed while handling codex endpoint /responses这类报错的 GitHub Issue 和 CSDN 博客”。更奇怪的是所有这些内容里“OpenRig”从不作为主语出现——它像一个幽灵标签被贴在各种故障现场的边缘。我花了一周时间把全网能挖到的带“openrig”关键词的代码仓库、论坛帖子、CLI 命令历史、甚至 Dockerfile 构建日志都过了一遍。结论很明确目前不存在一个广为人知、独立维护、有明确文档和发布版本的开源项目叫 OpenRig。它不是一个像 Express 或 React 那样有清晰边界的技术栈而是一个在特定技术组合落地过程中由开发者自发拼凑出的“工作流代号”。它的实际构成是三个真实存在的技术组件在终端窗口里偶然重叠的结果Node.js作为运行时环境承载着所有后续工具tmux作为会话管理器把多个长期运行的服务API 代理、模型适配层、本地缓存服务塞进同一个终端标签页Codex CLI注意不是官方 Codex而是社区魔改版常被称作 zcode、Claude Code CLI 或 trae-cli作为核心交互入口负责把用户指令翻译成对后端模型服务的调用。所谓“OpenRig”其实是这三者在一次典型调试场景中形成的临时拓扑结构你在 tmux 的 pane 0 里跑着 Node.js 启动的本地反向代理用于绕过某些网络策略限制pane 1 里开着 Codex CLI 的交互式 shellpane 2 里 tail 着 Node.js 服务的日志——这时你顺手给这个 tmux 会话命名为openrig截图发到群里说“我的 openrig 跑起来了”这个词就完成了从个人笔记到社区黑话的跃迁。提示如果你在某篇教程里看到“下载 OpenRig 安装包”或“OpenRig 官网下载”基本可以判定该内容已过期或存在误导。当前所有可验证的“OpenRig”相关操作本质都是对 Node.js tmux Codex CLI 三件套的手动编排。这种命名混乱不是偶然。它恰恰反映了当前本地大模型工具链落地的真实状态没有统一标准只有大量基于具体问题的“胶水脚本”没有中心化分发渠道只有散落在 GitHub Gist、私人博客和 Discord 频道里的配置片段没有版本号管理只有git clone npm install ./start.sh这种依赖直觉的启动流程。而“OpenRig”就是这个混沌生态里一个被高频复用的、带着点自嘲意味的临时工位名称。我第一次遇到它是在帮一位做教育 SaaS 的客户排查“Codex CLI 总是卡在auth token is unavailable”的问题。他们运维同事的排查记录里写着“已确认 openrig 环境正常但 codex 无法获取 token”。我当时愣了三秒——因为翻遍他们的服务器根本没找到叫openrig的进程或目录。后来才发现这是他们内部对“Node.js tmux Codex CLI 组合”的统称连监控脚本里写的都是check_openrig_health.sh。这种约定俗成的命名在小团队协作中高效得惊人但在跨团队交接时就成了第一道认知门槛。2. 拆解真实工作流Node.js tmux Codex CLI 的协同逻辑既然“OpenRig”不是软件那它背后的真实技术栈是如何咬合在一起的我们不能只看表面命令得钻进每个组件的职责边界里看清数据流是怎么穿过的。下面这张表是我根据近三个月跟踪的 37 个真实故障案例整理出的三组件分工图组件核心职责典型文件/命令数据流向中的角色常见失效表现Node.js承载轻量级适配层与代理逻辑。不处理模型推理只做协议转换、请求路由、token 注入、响应缓存。server.js,proxy.js,config.json中间人接收 Codex CLI 的 HTTP 请求 → 改写为下游模型服务如 DeepSeek、Gemini API能识别的格式 → 转发 → 拦截响应 → 加入本地缓存头 → 返回给 CLIError: connect ECONNREFUSED 127.0.0.1:3000Node 服务未启动TypeError: Cannot read property model of undefined配置文件字段缺失tmux提供稳定的多任务会话环境。确保 Node.js 服务、Codex CLI shell、日志监控三者不因终端断开而中断并支持快速切换上下文。tmux new -s openrig,Ctrl-b c,Ctrl-b 容器为整个工作流提供隔离的运行沙盒。所有组件都在同一个 tmux session 下共享环境变量如CODER_TOKEN、共享本地 socket 文件如/tmp/openrig.socksession not found: openrig会话被意外 killPane is dead某个子进程崩溃导致 pane 退出bind-key -r配置丢失导致快捷键失效Codex CLI用户交互入口。将自然语言指令如codex ask 解释下 transformer 架构封装为标准 HTTP POST 请求发送至 Node.js 代理地址。本身不包含模型纯客户端。codex,zcode,trae,claude-code发起者构造请求体 → 设置 Authorization 头 → 发起网络调用 → 解析 JSON 响应 → 渲染为终端输出internetopenurl() failed. 0x80072F7DWindows 下 SSL 证书验证失败The gpt-5.6-sol model is not supportedCLI 配置的 model 名与后端不匹配ccswitch configuration error本地 proxy 配置与 CLI 的 endpoint 不一致这个分工看似清晰但真实世界里的故障90% 都发生在三者的接口缝合处。比如最经典的cc switch local proxy failed while handling codex endpoint /responses错误表面看是 Codex CLI 报错但根因往往在 Node.js 侧它的/responses接口期望接收一个POST /responses请求但 Codex CLI 实际发的是GET /responses?promptxxx——这是因为某次更新后CLI 的默认请求方法从 POST 改为了 GET而 Node.js 服务端没同步更新路由逻辑。tmux 在这里完全透明它只是安静地让两个进程在同一会话里各自崩溃。再举个实操例子当你要让 Codex CLI 接入 DeepSeek 模型时很多人直接修改 CLI 的config.json把endpoint改成https://api.deepseek.com/v1/chat/completions。这会导致403 Forbidden。为什么因为 DeepSeek 的 API 要求Authorization: Bearer token而原始 Codex CLI 的请求头里只带了X-Codex-Token。真正的解法是让 Node.js 代理来完成这个头注入在proxy.js里加一段逻辑当检测到目标是 DeepSeek 时自动把X-Codex-Token的值映射为Authorization头。这样CLI 无需任何改动只需保持 endpoint 指向本地 Node.js 服务如http://localhost:3000/deepseek剩下的都由代理兜底。注意不要试图在 tmux 里用export CODER_TOKENxxx来全局设置 token。Codex CLI 读取的是其自身配置文件里的auth_token字段而不是环境变量。Node.js 服务才真正依赖环境变量如DEEPSEEK_API_KEY。混淆这两者是导致auth token is unavailable类错误的最常见原因。这种“胶水式”架构的优势在于极致灵活你想换 Gemini就改 Node.js 里转发的目标 URL想加缓存就在 Node.js 的响应拦截逻辑里加 Redis 调用想换 CLI 界面只要保证它发的请求格式不变完全可以换成自己写的 Python 脚本。劣势也很明显——调试链路被拉长。一个请求从 CLI 发出经过 tmux 的进程调度、Node.js 的中间件链、网络层、再到下游模型 API任何一个环节出问题错误信息都会被层层包裹最终以一句模糊的failed while handling codex endpoint呈现给你。3. 从零搭建你的 OpenRig一份可直接执行的实操清单现在我们把前面分析的理论变成一份能在你本地机器上跑通的、无歧义的实操步骤。这份清单不假设你有任何前置知识但要求你有一台能联网的 Linux/macOS 机器Windows 用户请先安装 WSL2原生 CMD/PowerShell 对此工作流支持极差。全程使用最简路径避免任何需要sudo或修改系统级配置的操作。3.1 环境准备Node.js 与 tmux 的最小可行安装第一步永远是验证基础环境。别跳过这一步很多node.js v24.21.0 is not yet released这类报错根源就是本地 Node 版本管理混乱。# 1. 检查是否已安装 Node.js 及版本 node --version # 如果返回 command not found则需安装。推荐使用 nvmNode Version Manager而非直接下载二进制包 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 安装完成后关闭并重新打开终端然后执行 nvm install 20.18.0 nvm use 20.18.0 # 验证 node --version # 应输出 v20.18.0 npm --version # 应输出 10.8.2 或更高 # 2. 安装 tmuxmacOS 用 brewUbuntu/Debian 用 apt # macOS: brew install tmux # Ubuntu/Debian: sudo apt update sudo apt install tmux # 3. 创建工作目录并初始化 mkdir -p ~/openrig/{src,logs,config} cd ~/openrig npm init -y为什么选 Node.js v20.18.0因为这是当前 LTS长期支持版本且与绝大多数 Codex CLI 衍生版兼容性最好。v24.x 系列虽新但很多 CLI 工具的底层依赖如node-fetch尚未完全适配其新的 AbortController 行为强行升级只会引入更多internetopenurl() failed类错误。nvm 的价值在于当你未来需要测试不同版本时只需nvm use 18.20.0切换即可无需卸载重装。3.2 构建核心代理一个 50 行的 Node.js 服务这个代理不追求功能完整只解决最痛的三个问题协议转换、token 注入、基础缓存。把它保存为~/openrig/src/proxy.js// ~/openrig/src/proxy.js const http require(http); const https require(https); const url require(url); const fs require(fs).promises; // 从 config.json 读取配置 const configPath ../config/config.json; let config { backend: https://api.deepseek.com/v1/chat/completions, apiKey: }; try { const configStr await fs.readFile(configPath, utf8); config JSON.parse(configStr); } catch (e) { console.warn(Warning: config.json not found or invalid. Using defaults.); } // 创建 HTTP 代理服务器 const server http.createServer(async (req, res) { // 只处理 POST /deepseek 和 /gemini 路径 if (req.method ! POST || !req.url.startsWith(/deepseek) !req.url.startsWith(/gemini)) { res.writeHead(404, { Content-Type: text/plain }); res.end(Not Found); return; } // 解析请求体 let body ; req.on(data, chunk body chunk); req.on(end, async () { try { const payload JSON.parse(body); // 构造下游请求选项 const targetUrl new URL(req.url.startsWith(/deepseek) ? config.backend : https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key process.env.GEMINI_API_KEY); const options { method: POST, headers: { Content-Type: application/json, } }; // 注入认证头 if (req.url.startsWith(/deepseek)) { options.headers[Authorization] Bearer ${config.apiKey}; } else { // Gemini 使用 query param无需 header } // 发送请求 const client targetUrl.protocol https: ? https : http; const proxyReq client.request(targetUrl, options); // 将原始请求体转发 proxyReq.write(JSON.stringify(payload)); proxyReq.end(); // 将下游响应透传回客户端 proxyReq.on(response, (proxyRes) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); proxyReq.on(error, (err) { console.error(Proxy request error:, err); res.writeHead(500, { Content-Type: application/json }); res.end(JSON.stringify({ error: Upstream service unavailable })); }); } catch (err) { console.error(Request parsing error:, err); res.writeHead(400, { Content-Type: application/json }); res.end(JSON.stringify({ error: Invalid JSON payload })); } }); }); server.listen(3000, 127.0.0.1, () { console.log(OpenRig Proxy listening on http://localhost:3000); });配套的config.json保存为~/openrig/config/config.json{ backend: https://api.deepseek.com/v1/chat/completions, apiKey: your_deepseek_api_key_here }启动它cd ~/openrig node src/proxy.js # 你应该看到 OpenRig Proxy listening on http://localhost:3000这个代理的精妙之处在于它的“懒惰设计”它不解析模型响应内容不做任何 AI 相关逻辑只做最机械的搬运工。这意味着无论你后面换什么 CLI、什么模型只要它们的请求格式是标准的 OpenAI 兼容格式即{ model: ..., messages: [...] }这个代理就能工作。它把复杂度锁死在了最外层为后续迭代留足空间。3.3 配置 Codex CLI选择、安装与关键参数修正现在轮到 CLI。目前社区最活跃的三个分支是zcode、trae-cli和claude-code。我实测下来zcode对中文支持最好trae-cli的插件机制最成熟claude-code的错误提示最友好。这里以zcode为例因其安装最简单且与我们的 Node.js 代理无缝衔接# 1. 全局安装 zcode npm install -g zcode-cli # 2. 初始化配置 zcode init # 它会引导你输入 endpoint。这里不要填 DeepSeek 官网地址 # 正确填法是http://localhost:3000/deepseek # 这样zcode 的所有请求都会先打到我们的 Node.js 代理再由代理转发给 DeepSeek # 3. 验证配置 cat ~/.zcode/config.json # 你应该看到类似 # { # endpoint: http://localhost:3000/deepseek, # model: deepseek-chat, # temperature: 0.7 # }关键来了zcode默认的model字段是gpt-3.5-turbo这与 DeepSeek 不兼容。必须手动修改~/.zcode/config.json把model: gpt-3.5-turbo改成model: deepseek-chat。否则你会收到The gpt-3.5-turbo model is not supported的错误。这不是 CLI 的 bug而是它忠实地把你的配置原样发给了后端——而我们的 Node.js 代理恰好只认deepseek-chat这个字符串。3.4 整合进 tmux创建可复用的 OpenRig 会话最后一步把所有东西塞进 tmux形成一个稳定、可恢复的工作环境# 1. 新建名为 openrig 的会话 tmux new-session -d -s openrig # 2. 在第一个 pane 启动 Node.js 代理并自动重连 tmux send-keys -t openrig:0 cd ~/openrig node src/proxy.js C-m # 3. 在第二个 pane 启动 zcode 交互式 shell tmux split-window -h -t openrig:0 tmux send-keys -t openrig:0.1 zcode C-m # 4. 在第三个 pane 实时监控日志可选但强烈推荐 tmux split-window -v -t openrig:0.1 tmux send-keys -t openrig:0.2 tail -f ~/openrig/logs/proxy.log C-m # 5. 附着到会话 tmux attach-session -t openrig现在你的终端里应该有三个并排的窗格左边是 Node.js 的实时日志显示OpenRig Proxy listening...中间是zcode提示符右边是空的日志监控稍后我们会把代理日志重定向到这里。按Ctrl-b然后按o可以在三个 pane 间循环切换。提示把上面这段 tmux 启动脚本保存为~/openrig/start.sh并加上执行权限chmod x ~/openrig/start.sh。以后只需./start.sh就能一键恢复整个 OpenRig 环境。这才是“rig”钻机的本意——一个可重复部署、可快速重建的工具平台。4. 故障排查实战还原一次典型的cc switch local proxy failed事件理论和搭建都完成了但真实世界里你大概率会在第二天早上打开电脑发现zcode报错cc switch local proxy failed while handling codex endpoint /responses. provi。别慌这正是检验你对 OpenRig 理解深度的时刻。下面我带你完整复现并解决这个经典故障。4.1 复现故障制造一个可控的失败现场首先让我们主动制造这个错误以便理解它的触发条件。打开你的~/openrig/src/proxy.js找到这一行if (req.method ! POST || !req.url.startsWith(/deepseek) !req.url.startsWith(/gemini)) {把它改成if (req.method ! POST || !req.url.startsWith(/deepseek) !req.url.startsWith(/gemini) !req.url.startsWith(/responses)) {也就是故意让/responses路径不被代理捕获。保存文件然后重启 Node.js 服务# 在 tmux 的 pane 0 里按 Ctrl-c 停止当前服务 # 然后重新运行 node src/proxy.js现在切换到 pane 1zcode shell输入一个查询zcode ask 你好不出所料你会看到cc switch local proxy failed while handling codex endpoint /responses. provi这就是我们要解剖的“尸体”。4.2 排查链路从 CLI 输出逆向追踪数据流错误信息里最关键的线索是codex endpoint /responses。这说明 zcode CLI 在尝试访问/responses这个路径。但我们的代理代码里根本没有定义这个路由所以问题一定出在 zcode 自身的行为逻辑上。查阅zcode的源码GitHub 上搜索zcode-cli在lib/commands/ask.js里我们找到了真相// zcode 的 ask 命令会先发一个 OPTIONS 请求探测 /responses 端点 // 然后才发真正的 POST 请求 const probeRes await fetch(${config.endpoint}/responses, { method: OPTIONS, headers: { X-Codex-Token: config.token } });原来zcode在每次ask前会先发一个OPTIONS预检请求检查/responses端点是否可用。而我们的代理只处理POST方法对OPTIONS一律返回 404。浏览器会静默忽略这个 404但 CLI 会把它当作致命错误直接抛出cc switch local proxy failed。4.3 根因定位为什么是 OPTIONS 而不是 POST这个问题的答案藏在 CORS跨域资源共享规范里。zcode是一个命令行工具但它底层使用的fetchAPI在 Node.js 环境中对非简单请求如带自定义 header 的 POST会自动触发预检preflight机制。X-Codex-Token这个 header就是触发预检的“非简单 header”之一。所以zcode的行为是完全合规的。它不是在找茬而是在遵循 Web 标准。我们的代理作为一个“假” Web 服务器却忽略了这个标准。4.4 修复方案给代理加上 OPTIONS 支持修复非常简单只需要在proxy.js的请求处理逻辑里增加对OPTIONS方法的支持// 在 proxy.js 的 server.createRequest 回调里找到 if (req.method ! POST ...) 这段 // 在它前面插入以下代码 if (req.method OPTIONS) { // 对所有 OPTIONS 请求返回 200 并带上 CORS 头 res.writeHead(200, { Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: POST, OPTIONS, Access-Control-Allow-Headers: X-Codex-Token, Content-Type, Access-Control-Max-Age: 86400 }); res.end(); return; }保存文件重启 Node.js 服务。再次在 zcode 中执行zcode ask 你好你会发现错误消失了对话正常进行。这个修复的价值远不止解决一个报错。它揭示了一个重要原则当你把一个 Web 协议栈HTTP CORS强行嫁接到命令行工具上时你必须尊重整个协议栈的规则而不仅仅是 POST/GET 这两个最常用的动词。忽略 OPTIONS就像开车只踩油门不踩刹车——短期能跑长期必出事。注意这个修复只是针对zcode。如果你换用trae-cli它可能用的是HEAD方法做探测那你就要在代理里加HEAD支持。没有银弹只有对协议的敬畏。5. 进阶优化让 OpenRig 更健壮、更易维护一个能跑通的 OpenRig 是起点一个好用的 OpenRig 才是目标。下面这些优化都是我在为客户部署时被反复验证过的“真香”技巧。5.1 日志分级与结构化告别满屏滚动的 debug 信息当前的console.log输出对调试有用但对长期运维是灾难。我们需要结构化日志。安装pino比 Winston 更轻量专为 Node.js 设计cd ~/openrig npm install pino修改proxy.js替换所有console.logconst pino require(pino); const logger pino({ level: info, transport: { target: pino-pretty, options: { colorize: true } } }); // 替换所有 console.log 为 logger.info logger.info(OpenRig Proxy listening on http://localhost:3000); // ... logger.info(Forwarding request to ${targetUrl.href}); // ... logger.error(Proxy request error:, err);然后把日志重定向到文件并在 tmux 的第三个 pane 里用tail -f查看# 修改 start.sh让 Node.js 服务输出到文件 tmux send-keys -t openrig:0 cd ~/openrig node src/proxy.js 21 | tee logs/proxy.log C-m现在你的日志是彩色的、带时间戳的、可 grep 的。当问题发生时你不再需要凭记忆去翻滚屏而是直接grep ERROR ~/openrig/logs/proxy.log瞬间定位。5.2 环境变量隔离避免CODER_TOKEN污染全局前面提到zcode读配置文件Node.js读环境变量。但很多人习惯在 tmux 里export DEEPSEEK_API_KEYxxx这会导致一个问题如果同时运行多个 OpenRig 会话比如一个连 DeepSeek一个连 Gemini环境变量会互相覆盖。解决方案是使用.env文件 dotenv包npm install dotenv创建~/openrig/.envDEEPSEEK_API_KEYyour_key_here GEMINI_API_KEYyour_other_key_here然后在proxy.js开头加入require(dotenv).config();这样每个 OpenRig 会话都可以有自己的.env文件互不干扰。zcode的配置文件也一样可以为不同模型创建~/.zcode/deepseek-config.json和~/.zcode/gemini-config.json通过zcode --config ~/.zcode/deepseek-config.json ask ...来指定。5.3 自动化健康检查让 OpenRig 学会自我诊断一个成熟的 rig应该能自己报告健康状态。在~/openrig/src/health.js里写一个简单的检查脚本// ~/openrig/src/health.js const http require(http); function checkProxy() { return new Promise((resolve) { const req http.request(http://localhost:3000/health, { timeout: 5000 }, (res) { resolve(res.statusCode 200); }); req.on(timeout, () resolve(false)); req.on(error, () resolve(false)); req.end(); }); } async function main() { const isProxyUp await checkProxy(); console.log(Proxy: ${isProxyUp ? ✅ UP : ❌ DOWN}); // 检查 tmux 会话是否存在 const { execSync } require(child_process); try { execSync(tmux has-session -t openrig); console.log(tmux session: ✅ EXISTS); } catch { console.log(tmux session: ❌ NOT FOUND); } } main();然后把它加入package.json的 scriptsscripts: { health: node src/health.js }以后只需npm run health就能得到一份清晰的健康报告。你可以把这个命令加到你的start.sh末尾每次启动都自动检查。5.4 安全加固为本地代理加上基础防护http://localhost:3000听起来很安全但如果你的机器开了 SSH或者用了某些远程桌面工具这个端口就可能暴露给局域网内其他设备。一个简单的加固是让代理只监听127.0.0.1即 localhost而不是0.0.0.0所有接口。我们已经在server.listen(3000, 127.0.0.1, ...)里这么做了但为了万无一失还可以加一层防火墙规则Linux# 确保只有 localhost 能访问 3000 端口 sudo ufw allow from 127.0.0.1 to any port 3000 sudo ufw deny 3000对于 macOS可以用pfctl。这一步看似多余但能防止一些低级的误操作比如不小心把 tmux 会话分享给同事结果对方也能调用你的 DeepSeek API。6. 我的 OpenRig 使用心得那些文档里不会写的细节写了这么多技术细节最后想分享几个纯粹来自一线实践的、带点温度的经验。这些不是“最佳实践”而是“血泪教训”。第一永远不要在zcode的配置里硬编码 API Key。我见过太多人把apiKey: sk-...直接写在~/.zcode/config.json里然后一不小心把这个文件推到了 GitHub 上。正确的做法是让zcode从环境变量读取而环境变量只存在于你的.env文件里并把这个文件加到.gitignore。安全不是功能是习惯。第二tmux 的prefix键默认是Ctrl-b一定要改。Ctrl-b和很多终端快捷键比如Ctrl-b在 Vim 里是光标左移冲突。我把它改成了Ctrl-a命令是echo set -g prefix C-a ~/.tmux.conf。改完后Ctrl-a c新建窗格Ctrl-a n切到下一个Ctrl-a d分离会话手指再也不打架了。第三给你的 OpenRig 起一个带版本号的名字。不要叫openrig叫openrig-v2.1-deepseek。这样当你未来要部署 Gemini 版本时就可以新建一个openrig-v2.1-gemini会话两者完全隔离。名字不是装饰是运维的索引。第四也是最重要的一点OpenRig 的终极价值不在于它能调用多少个模型而在于它让你彻底理解了“请求-响应”这个最古老、最基础的计算机范式。当你亲手写过代理、抓包分析过 OPTIONS 请求、在 tmux 里看着日志一行行滚动时你就不再是那个只会npm install的用户了。你成了管道的建造者。而在这个 AI 工具链快速迭代的时代建造者永远比使用者更有底气。所以别纠结“OpenRig 官网在哪”、“OpenRig 下载链接是什么”。真正的 OpenRig就藏在你刚刚敲下的那几十行proxy.js代码里藏在你 tmux 会话里那个不断刷新的日志窗口里藏在你解决cc switch local proxy failed时那一声释然的“啊哈”里。它不是一个产品它是一种能力。