
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 封装成命令行工具后随手命名为 zcodez 代表 zero-latencycode 是 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 LTS18.20.4是目前最稳妥的选择因为它原生支持fetch和AbortSignal.timeout()无需额外 polyfillprocess.env的继承行为在子进程 spawn 时更稳定这对后续调用 tmux 命令至关重要npm v9.x 对 workspace 和 overrides 的处理比 v10 更兼容老旧的 CLI 封装脚本。安装时务必避开官网下载页的“Current”版本常为不稳定预发版。正确做法是访问 https://nodejs.org/dist/ 手动下载node-v18.20.4-linux-x64.tar.xzLinux或node-v18.20.4-win-x64.zipWindows解压后通过软链接方式注入 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 流式响应监听器时如果直接在前台运行CtrlC 会终止整个进程而用 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 paneconst { 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 的鼠标事件支持不完整CtrlArrow切换 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/completions404 是必然结果。更深层的问题在于 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 流程和 endpointCodex 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和本地 MacVentura双环境实测。全程耗时约 12 分钟成功后你将拥有一个支持多会话、自动日志、错误熔断的本地 Codex 工具链。5.1 环境初始化四步锁定基础栈安装 Node.js v18.20.4Linuxwget 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 titlessudo 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-cli5.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 sessionNode.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-middleware5.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.jsconst { 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.js5.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” 真正该有的样子不是某个神秘工具而是一套可理解、可审计、可协作的工作方法论。