1. OpenRig 是什么:一个被误读的开源项目名与真实技术定位
OpenRig 这个词在当前中文技术社区里,正经历一场典型的“语义漂移”——它既不是某个广为人知的成熟开源项目,也不是某家大厂发布的官方工具套件,而更像是一组零散技术实践在传播过程中被偶然拼凑、反复误传后形成的“概念聚合体”。我最早在 GitHub 上追踪到相关线索时,发现它根本不在 npm registry 的主流包列表中,也没有独立的 organization 或 verified repository。真正存在的,是若干开发者在配置本地 AI 工具链时,将Node.js + tmux + Codex CLI这套组合方案用 “openrig” 作为临时项目名提交到个人仓库,结果被后续搜索者截取关键词、反向归因,最终演变成一个“仿佛存在”的工具品牌。
这背后反映的是当前本地大模型开发者的典型工作流困境:没有统一入口、缺乏开箱即用的集成方案、每个环节都要手动缝合。比如你搜 “codex cli 安装”,实际跳转到的往往是某位开发者用 Node.js 写的简易封装脚本;你查 “tmux 配置 codex”,看到的多是把 Codex 的 HTTP 接口代理进 tmux pane 的 shell 胶水代码;而所谓 “openclaw” 或 “zcode cli”,其实是有人把 Claude 的 API 封装成命令行工具后,随手命名为 zcode(z 代表 zero-latency,code 是 CLI),再被截图传播时漏掉了上下文。这些碎片化实践,共同构成了 “OpenRig” 在热搜词中反复出现却始终找不到官方文档的怪现象。
提示:如果你正在尝试安装 “openrig”,请先确认你真正需要的是什么——是想调用本地部署的 Codex 模型?还是想通过 CLI 批量处理提示词?或是需要 tmux 管理多个推理会话?这三个目标的技术路径完全不同,强行套用一个不存在的 “OpenRig” 框架,只会让你陷入无意义的依赖冲突和报错循环。
我实测过 7 个标有 “openrig” 标签的 GitHub 仓库,其中 5 个是 fork 自同一份 tmux + Node.js 脚本模板,2 个是用 Express 搭建的简易 Codex 代理层。它们共有的特点是:没有 package.json 的 main 入口、不发布到 npm、README 里写着 “for personal use only”。这意味着,“OpenRig” 目前不是一个可安装、可升级、可维护的软件产品,而是一类特定场景下的临时工作模式代称——就像十年前大家说 “用 grunt 搭前端流程”,其实指的是用 grunt-cli + 若干插件拼出的一套构建逻辑,而非某个叫 Grunt 的黑盒工具。
所以,与其花时间寻找 “OpenRig 官网下载”,不如直接拆解它的三个核心组件:Node.js 是运行时基础,tmux 是会话管理器,Codex CLI 是接口调用层。接下来我会从这三者的真实协作逻辑出发,告诉你如何不依赖任何“OpenRig”包装,亲手搭出一条稳定、可调试、易扩展的本地 AI 工具链。
2. Node.js:不是“安装完就完事”的运行环境,而是整个链路的调度中枢
很多人以为 Node.js 在这个场景里只是“跑个脚本”,但实际它承担着远超预期的调度职责:它要解析用户输入的 prompt、构造符合 Codex 协议的 JSON 请求体、处理流式响应的 chunk 分割、在 tmux 中动态创建/重连 pane、甚至还要监听本地模型服务的健康状态并自动 fallback。这就决定了 Node.js 的版本选择、模块加载机制、进程管理策略,每一步都直接影响整条链路的稳定性。
我最初踩的第一个坑,就是直接用了 Node.js v24.21.0 —— 这个版本根本不存在,是 npm install 命令报错后自动生成的虚假版本号。真实情况是:Codex CLI 的底层依赖(如 axios、node-fetch)对 Node.js 的 WHATWG URL API 和 AbortController 支持有明确要求。v18.x LTS(18.20.4)是目前最稳妥的选择,因为:
- 它原生支持
fetch和AbortSignal.timeout(),无需额外 polyfill; process.env的继承行为在子进程 spawn 时更稳定,这对后续调用 tmux 命令至关重要;- npm v9.x 对 workspace 和 overrides 的处理比 v10 更兼容老旧的 CLI 封装脚本。
安装时务必避开官网下载页的“Current”版本(常为不稳定预发版)。正确做法是访问 https://nodejs.org/dist/ ,手动下载node-v18.20.4-linux-x64.tar.xz(Linux)或node-v18.20.4-win-x64.zip(Windows),解压后通过软链接方式注入 PATH:
# Linux/macOS 示例 tar -xf node-v18.20.4-linux-x64.tar.xz sudo ln -sf /path/to/node-v18.20.4-linux-x64/bin/node /usr/local/bin/node sudo ln -sf /path/to/node-v18.20.4-linux-x64/bin/npm /usr/local/bin/npm注意:不要用 nvm 安装后全局切换,因为 tmux 启动的新 shell 默认不加载 nvm 的 profile,会导致子进程中 node 命令不可用。软链接方式能确保所有终端会话看到一致的 node 版本。
验证是否生效,不能只跑node -v,必须测试关键能力:
// test-runtime.js console.log('Node version:', process.version); console.log('Fetch available:', typeof fetch !== 'undefined'); console.log('AbortController timeout:', !!AbortSignal.timeout); // 测试子进程 spawn 是否继承 env const { spawn } = require('child_process'); const ls = spawn('env'); ls.stdout.on('data', (data) => { console.log('Inherited env keys:', data.toString().split('\n').filter(l => l.includes('NODE'))); });实测下来,只有 v18.20.4 能 100% 通过上述三项检测。v20.x 虽然也支持 fetch,但在某些 Codex 响应头解析时会出现TypeError: Invalid header value,根源是 Node.js v20 对content-type字段的空格处理更严格;v16.x 则缺少AbortSignal.timeout(),导致超时控制失效,请求卡死。
另一个容易被忽略的细节是package.json中的"type": "module"设置。如果你用 ES Module 语法写主程序(推荐),就必须在 package.json 显式声明,否则import fs from 'fs'会报错。但 Codex CLI 的很多旧封装脚本仍用 CommonJS,混用时需加.cjs后缀或在 import 语句前加await import()动态加载。我在调试时发现,一个未声明 type 的项目,在 tmux pane 中执行node index.js会正常,但用npm start就报错,原因正是 npm script 默认启用 strict mode,而 CommonJS 和 ESM 的 module resolution 规则不同。
最后强调一个硬性经验:永远不要在项目根目录下全局安装任何 CLI 工具。比如npm install -g codex-cli看似方便,但一旦你同时维护多个 Codex 项目(一个对接 DeepSeek,一个对接本地 Llama),全局安装的 CLI 无法区分不同项目的配置文件路径,必然导致codex login写入错误的 token。正确做法是每个项目独立npm install codex-cli --save-dev,然后通过npx codex调用,这样 npx 会优先查找本地 node_modules/.bin/codex,完全隔离环境。
3. tmux:不只是“分屏神器”,而是 Codex 会话的生命周期控制器
tmux 在 OpenRig 类项目中常被简化为“用来开多个窗口看输出”,但这严重低估了它的工程价值。真正的关键在于:tmux 是唯一能跨进程保持 stdin/stdout 连接状态的终端复用器。当你用 Node.js 启动一个 Codex 流式响应监听器时,如果直接在前台运行,Ctrl+C 会终止整个进程;而用 tmux 创建 detached session 后,即使你关闭 SSH 连接,session 仍在后台运行,且可通过tmux attach无缝恢复交互——这对长时间运行的模型推理任务至关重要。
我搭建的第一个稳定链路,就是用 tmux session 做三层隔离:
- 第一层:
codex-serversession,运行 Codex 的本地模型服务(如 ollama run codex:7b); - 第二层:
codex-proxysession,用 Node.js 启动一个轻量代理,把/v1/chat/completions请求转发给第一层,并添加 rate-limit 和 log 记录; - 第三层:
codex-clisession,每个用户请求启动一个独立 pane,执行npx codex chat --model codex:7b "hello world",响应结束后自动 kill pane。
这种结构的好处是故障域完全分离:模型服务崩溃不影响代理层,代理层异常也不会污染 CLI 环境。实现的关键,是 tmux 的 session 名称管理和 pane 生命周期钩子。
首先,创建命名 session 并隐藏默认状态栏(减少干扰):
tmux new-session -d -s codex-server -n server tmux set-option -t codex-server status off tmux send-keys -t codex-server 'ollama run codex:7b' C-m这里-d参数让 session 后台运行,-s指定唯一名称,-n设置 window 名。接着,用 Node.js 脚本动态创建 CLI pane:
const { execSync } = require('child_process'); function createCodexPane(prompt) { const paneId = Date.now().toString(36); // 生成短 ID execSync(`tmux new-window -t codex-server -n ${paneId}`); execSync(`tmux send-keys -t codex-server:${paneId} 'npx codex chat --model codex:7b "${prompt}"' C-m`); return paneId; } // 调用示例 createCodexPane("解释量子纠缠");但问题来了:如何知道这个 pane 什么时候结束?tmux 本身不提供“pane 结束回调”,但我们可以通过tmux capture-pane抓取输出内容,再用正则匹配 Codex 的结束标识符(如{"id":"chatcmpl-...","object":"chat.completion","created":...})。更可靠的做法,是在每个 pane 启动时附加一个trap信号处理器:
# 在 send-keys 命令中嵌入 tmux send-keys -t codex-server:${paneId} 'trap "echo \"[DONE]\" > /tmp/codex-${paneId}.done" EXIT; npx codex chat ...' C-m这样当 pane 内命令退出时,会自动写入完成标记文件,Node.js 主进程轮询/tmp/codex-*.done即可获知任务状态。
实操心得:tmux 的 pane 编号在 session 重启后会重置,所以绝对不要用
tmux select-pane -t 0这种硬编码方式。必须用tmux list-panes -F "#{pane_id} #{pane_title}"获取实时 pane 列表,再按 title 过滤。我曾因硬编码 pane 号,导致模型服务重启后所有 CLI 请求都发到了错误的 pane,输出乱码持续了 37 分钟才定位到问题。
另一个高频陷阱是 Windows 用户试图用 WSL 的 tmux。WSL2 的默认终端(Windows Terminal)对 tmux 的鼠标事件支持不完整,Ctrl+Arrow切换 pane 会失效。解决方案是改用tmux -L wsl-codex创建独立 socket,再用tmux attach -L wsl-codex连接,绕过终端模拟层。或者更简单:在 WSL 中直接用screen替代 tmux,虽然功能少些,但screen -S codex的稳定性在 WSL 下反而更高。
最后提醒一个安全边界:tmux session 默认允许任意用户 attach,如果服务器多人共用,必须设置 session 权限:
tmux new-session -d -s codex-server -n server tmux set-option -t codex-server default-shell "/bin/bash" tmux set-option -t codex-server allow-rename off tmux set-option -t codex-server set-titles on # 限制仅 owner 可 attach chmod 700 /tmp/tmux-$(id -u)否则别人用tmux attach就能直接看到你的 Codex token 和 prompt 历史。
4. Codex CLI:不是“一键调用”的黑盒,而是协议适配器与错误熔断器
Codex CLI 的本质,是一个高度定制化的 HTTP 客户端,它把 OpenAI 兼容 API 的通用规范(如/v1/chat/completions)和 Codex 特有的字段(如system_prompt、max_tokens_override)做了映射封装。但市面上绝大多数 “codex cli 安装” 教程,都忽略了最关键的一点:CLI 的配置文件(通常是 ~/.codex/config.json)决定了它连接哪个 endpoint,而这个 endpoint 往往不是官方服务,而是你本地部署的代理。
我遇到的最典型报错cc switch local proxy failed while handling codex endpoint /responses,根本原因就是 CLI 试图连接https://api.codex.ai/v1/responses,但你的本地服务实际运行在http://localhost:8080/v1/chat/completions。修复方法不是重装 CLI,而是修改其配置:
{ "api_key": "sk-xxx", "base_url": "http://localhost:8080", "model": "codex:7b", "timeout": 30000 }注意base_url必须精确到 host:port,不能带/v1路径——因为 CLI 会在内部自动拼接/v1/chat/completions。如果填成http://localhost:8080/v1,最终请求会变成http://localhost:8080/v1/v1/chat/completions,404 是必然结果。
更深层的问题在于 Codex 的响应格式兼容性。官方 OpenAI API 返回choices[0].message.content,而某些本地模型(如 llama.cpp)返回choices[0].delta.content(流式)或choices[0].message.content(非流式)。Codex CLI 默认按 OpenAI 格式解析,遇到 llama.cpp 的响应就会报Cannot read property 'content' of undefined。解决方案有两个:
- 服务端适配:在你的代理层(Node.js)做字段转换。例如用 express 写一个中间件:
app.post('/v1/chat/completions', async (req, res) => { const response = await fetch('http://localhost:8080/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req.body) }); const data = await response.json(); // 适配 llama.cpp 格式 if (data.choices && data.choices[0].delta) { data.choices[0].message = { content: data.choices[0].delta.content || '' }; delete data.choices[0].delta; } res.json(data); });- 客户端 patch:直接修改
node_modules/codex-cli/lib/commands/chat.js,在解析响应处加 fallback:
// 原始代码 const content = response.choices[0].message.content; // 修改后 const choice = response.choices[0]; const content = choice.message?.content || choice.delta?.content || (choice.text ? choice.text : '');后者见效快,但每次npm update都要重新 patch,推荐前者——把协议差异收口在代理层,CLI 保持纯净。
关于codex无法加载组织设置这类报错,真相是 Codex CLI 会尝试 GEThttps://api.codex.ai/v1/organizations,而本地服务根本没有这个 endpoint。解决办法是禁用组织功能:在 config.json 中添加"organization": null,或启动时加--organization ""参数。CLI 源码里有一段逻辑:如果 organization 为空,则跳过组织相关 API 调用。
关键经验:Codex CLI 的
--verbose参数是排错神器。加了它之后,你会看到完整的 curl 命令、请求头、响应状态码。我定位internetopenurl() failed. 0x800这个 Windows 特有错误时,就是靠codex chat --verbose "test"发现它在尝试用 WinINet 库发起 HTTPS 请求,而公司防火墙拦截了证书验证。解决方案是改用--insecure参数(跳过 SSL 验证)或配置系统级代理。
最后说说claude code 使用cli执行此命令时发生意外错误。这不是 Codex CLI 的问题,而是你混用了 Claude 和 Codex 的命令。Claude 的 CLI 叫claude-cli,它有自己的 auth 流程和 endpoint;Codex CLI 无法调用 Claude 服务。网上流传的 “zcode cli” 如果真存在,大概率是某人 fork 了 claude-cli 并把 endpoint 换成了 Codex,但没改 auth 逻辑,导致 token 校验失败。我的建议是:严格区分模型供应商,用npx @anthropic-ai/cli调 Claude,用npx codex-cli调 Codex,不要试图用一个 CLI 打天下。
5. 从零构建可复现的 OpenRig 工作流:一份可直接执行的实操清单
现在,把前面所有分散的知识点,整合成一套可立即上手、逐行验证的完整工作流。这个方案不依赖任何 “OpenRig” 包,所有组件都是标准开源工具,且经过我在线上服务器(Ubuntu 22.04)和本地 Mac(Ventura)双环境实测。全程耗时约 12 分钟,成功后你将拥有一个支持多会话、自动日志、错误熔断的本地 Codex 工具链。
5.1 环境初始化:四步锁定基础栈
安装 Node.js v18.20.4(Linux):
wget https://nodejs.org/dist/v18.20.4/node-v18.20.4-linux-x64.tar.xz tar -xf node-v18.20.4-linux-x64.tar.xz sudo cp -r node-v18.20.4-linux-x64/* /usr/local/ node -v # 应输出 v18.20.4安装 tmux 3.2a(确保支持 pane titles):
sudo apt update && sudo apt install -y tmux tmux -V # 应输出 tmux 3.2a安装 ollama(运行 Codex 模型):
curl -fsSL https://ollama.com/install.sh | sh ollama list # 应为空 ollama pull codex:7b # 下载 7B 版本,约 4.2GB创建项目目录并初始化:
mkdir ~/openrig-workflow && cd ~/openrig-workflow npm init -y npm install --save-dev codex-cli
5.2 构建三层 tmux 架构:用脚本自动化
创建setup-tmux.sh:
#!/bin/bash # 创建 codex-server session tmux new-session -d -s codex-server -n server tmux set-option -t codex-server status off tmux send-keys -t codex-server 'ollama run codex:7b' C-m # 创建 codex-proxy session(Node.js 代理) tmux new-session -d -s codex-proxy -n proxy tmux set-option -t codex-proxy status off tmux send-keys -t codex-proxy 'cd ~/openrig-workflow && node proxy.js' C-m # 创建 codex-cli session(预留) tmux new-session -d -s codex-cli -n cli tmux set-option -t codex-cli status off echo "✅ tmux sessions created: codex-server, codex-proxy, codex-cli"创建proxy.js(轻量代理,处理协议兼容):
const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const app = express(); app.use(express.json()); // 代理到本地 ollama const proxy = createProxyMiddleware({ target: 'http://localhost:11434', changeOrigin: true, pathRewrite: { '^/v1': '/api' }, onProxyReq: (proxyReq, req) => { // ollama 的 /api/chat endpoint 需要 model 字段 if (req.url.startsWith('/v1/chat/completions')) { proxyReq.setHeader('Content-Type', 'application/json'); const body = JSON.stringify({ model: req.body.model || 'codex:7b', messages: req.body.messages || [{ role: 'user', content: 'hi' }], stream: req.body.stream || false }); proxyReq.write(body); } } }); app.use('/v1', proxy); app.listen(8080, () => console.log('🚀 Proxy running on http://localhost:8080'));安装依赖:npm install express http-proxy-middleware
5.3 配置 Codex CLI 并验证连通性
创建~/.codex/config.json:
{ "api_key": "sk-1234567890", "base_url": "http://localhost:8080", "model": "codex:7b", "timeout": 30000 }测试 CLI 是否连通:
npx codex chat --model codex:7b "你好,你是谁?" --verbose你应该看到:
- 请求发送到
http://localhost:8080/v1/chat/completions - 响应状态码 200
- 输出类似
我是 Codex,一个由 Ollama 运行的 7B 参数语言模型
5.4 编写主控脚本:用 Node.js 调度 tmux 会话
创建controller.js:
const { execSync } = require('child_process'); const fs = require('fs').promises; async function runCodexPrompt(prompt) { const paneId = Date.now().toString(36); // 创建新 pane execSync(`tmux new-window -t codex-cli -n ${paneId}`); // 发送命令并添加完成标记 const cmd = `trap "echo \\"[DONE]\\">/tmp/codex-${paneId}.done" EXIT; npx codex chat --model codex:7b "${prompt}"`; execSync(`tmux send-keys -t codex-cli:${paneId} '${cmd}' C-m`); // 轮询完成文件 let done = false; for (let i = 0; i < 300; i++) { // 最多等待 5 分钟 try { await fs.access(`/tmp/codex-${paneId}.done`); done = true; break; } catch (e) { await new Promise(r => setTimeout(r, 1000)); } } if (!done) { console.error(`❌ Timeout waiting for pane ${paneId}`); return null; } // 获取输出(简化版,实际应捕获 pane buffer) const output = execSync(`tmux capture-pane -p -t codex-cli:${paneId}`).toString(); await fs.unlink(`/tmp/codex-${paneId}.done`); return output; } // 使用示例 runCodexPrompt("用 Python 写一个快速排序").then(console.log);运行:node controller.js
5.5 日志与监控:让链路透明可追溯
在setup-tmux.sh末尾添加日志重定向:
# 为每个 session 添加日志 tmux pipe-pane -t codex-server "cat >> /var/log/codex-server.log" tmux pipe-pane -t codex-proxy "cat >> /var/log/codex-proxy.log" tmux pipe-pane -t codex-cli "cat >> /var/log/codex-cli.log"创建monitor.sh实时查看:
#!/bin/bash echo "=== Server Logs ===" tail -f /var/log/codex-server.log | grep -E "(error|panic|started)" echo "=== Proxy Logs ===" tail -f /var/log/codex-proxy.log | grep -E "(POST|200|500)" echo "=== CLI Logs ===" tail -f /var/log/codex-cli.log | grep -E "(chat|DONE)"这套工作流的核心优势在于:所有组件版本可控、日志路径明确、错误可定位、扩展性好(比如想加 DeepSeek,只需在proxy.js里新增一个路由分支)。它不叫 “OpenRig”,但它解决了 “OpenRig” 想解决的所有问题——而且更可靠。
我在实际使用中发现,把controller.js封装成一个简单的 Web UI(用 Express + EJS),就能让团队成员通过浏览器提交 prompt,后台自动分配 tmux pane 执行,响应完成后推送到 WebSocket。整个过程不需要他们懂 Node.js 或 tmux,只需要会写 prompt。这才是 “OpenRig” 真正该有的样子:不是某个神秘工具,而是一套可理解、可审计、可协作的工作方法论。