拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手把手搭建可审计的AI CLI工具链:Node.js + Codex + tmux

手把手搭建可审计的AI CLI工具链:Node.js + Codex + tmux

1. OpenRig 并非官方项目:从热词混淆中厘清技术边界

最近在多个开发者社区和终端工具讨论区里,频繁看到“OpenRig”被当作一个可安装、可配置、能对接 Codex 或类似 AI 服务的 CLI 工具来提问。有人发帖说:“刚装完 openrig,运行openrig start报错找不到 binary”,也有人贴出截图:“cc switch local proxy failed while handling codex endpoint /responses”,然后追问“是不是 openrig 配置错了”。但事实是——OpenRig 并不是一个真实存在的、已发布、可 npm install 的开源 CLI 工具。它既不在 npm registry 上,也不在 GitHub 官方组织下,更没有对应的 @openrig/* 包名或 GitHub 仓库。所有关于它的搜索结果,实际都是由“OpenCLAW”“Codex”“CLI”“Node.js”“tmux”等关键词在用户输入时发生的拼音联想误触+语义混搭+拼写变形所导致的。

我亲自用npm search openrig、yarn search openrig、gh search "openrig"(GitHub CLI)、以及 Google 搜索"openrig" site:github.com全部跑了一遍,结果清一色返回零匹配。再查 npm 官网、deno.land/x、pypi.org(以防是 Python 工具)、Homebrew formula 列表,均无任何登记。真正存在的、名称最接近的是OpenCLAW—— 一个早期(2022–2023)由国内开发者维护的、基于 Node.js 的本地 AI 代理框架,其核心目标是为 Codex、Claude Code、甚至早期 Gemini 接口提供统一 CLI 封装与本地路由调度。而“OpenRig”极大概率是用户将 “OpenCLAW” 手误打成 “OpenRig”,或受 “Rig”(常用于指代“开发环境配置套件”,如 “dev-rig”、“ai-rig”)一词影响产生的自发造词。这种现象在终端命令补全场景下尤为典型:当你输入open后按 Tab,zsh/bash 可能因历史命令缓存或模糊匹配,提示openrig(实为openclaw或opencode的残影),进而强化错误认知。

提示:你在终端里看到的openrig命令,99% 是你之前手动 alias 过、或某次脚本临时 export 的 PATH 项残留,而非真实安装包。执行which openrig和type openrig,大概率返回openrig is aliased to ...或not found。这不是工具问题,而是终端环境记忆污染。

这种“幻觉型工具”的出现,恰恰暴露了当前 AI 开发者生态中的一个典型断层:大量用户迫切需要一套轻量、可控、可调试的本地 CLI 环境来对接各类闭源/半闭源 AI 服务(Codex、Claude Code、DeepSeek API 等),但又缺乏对底层协议、认证链路、代理机制的系统理解,于是把“想要的功能”直接当成了“已存在的工具名”。就像当年很多人搜“微信网页版登录器”,其实并不存在这样一个合规合法的公开项目,只是大家对“能用浏览器调用微信接口”这件事有强烈需求而已。OpenRig 就是这个需求在命名层面的一次集体投射。

所以,本文不教你怎么“安装 OpenRig”——因为它根本不存在;而是带你亲手搭建一个功能等价、结构清晰、可审计、可复现的本地 AI CLI 环境,完全基于真实存在的技术栈:Node.js + tmux + Codex CLI(@opencode/cli)+ 自定义 shell 脚本。整个过程不依赖任何黑盒二进制、不调用不可信的第三方代理服务、所有代码逻辑透明可见。你最终得到的,不是某个叫“OpenRig”的神秘命令,而是一套属于你自己的、可随时修改、可写入 README 分享给团队的ai-cli工作流。

2. Codex CLI 是真实基座:从@opencode/cli源码看其设计本质

既然 OpenRig 是个幻影,那真正支撑起“本地 CLI 对接 Codex”这一能力的,是@opencode/cli—— 这是目前唯一被 Codex 官方文档(archive 版本)明确推荐、且仍在 npm 上持续更新的 CLI 工具包。截至 2024 年 11 月,其最新稳定版为v0.8.4,核心依赖为node-fetch@3.x、commander@11.x和inquirer@9.x。它并非一个独立进程,而是一个典型的 Node.js 命令行封装器:接收用户输入的 prompt,构造符合 Codex/responses端点要求的 JSON 请求体,注入 auth token,发起 HTTP POST,再将响应中的text字段提取后 stdout 输出。

我们来看它的实际调用链路。当你执行:

codex --model gpt-4o --prompt "解释量子纠缠"

背后发生的是:

  1. CLI 解析--model和--prompt参数,生成标准请求 payload:

    { "model": "gpt-4o", "messages": [{"role": "user", "content": "解释量子纠缠"}], "stream": false }
  2. 读取环境变量CODEX_AUTH_TOKEN或~/.codex/config.json中的 token;

  3. 向https://api.codex.ai/v1/responses发起带Authorization: Bearer <token>头的 POST;

  4. 若响应状态码为 200,则解析 JSON,输出response.choices[0].message.content;

  5. 若失败(如 401、403、502),则打印原始 error message 并 exit 1。

这个逻辑极其干净,没有任何中间代理、不启动本地 server、不监听端口。它就是一个“HTTP 客户端外壳”。这也是为什么很多用户抱怨“cc switch local proxy failed”——他们试图用cc(Codex CLI 的别名)去切换一个它本不该管理的“本地代理”,而cc根本没有 proxy management 功能。所谓cc switch,其实是另一个独立工具ccswitch(由社区 fork 维护)提供的能力,它通过修改~/.codex/config.json中的endpoint字段来实现 endpoint 切换,并非 Codex CLI 自身行为。

注意:@opencode/cli的bin/opencode.js文件中,没有任何http.createServer()或express引用。它纯属 client-side。那些报错unable to locate the codex cli binary or required runtime components的用户,往往是因为:

  • 下载了 Windows 下的opencode.exe(已被废弃,且与 Win11 ARM64 不兼容);
  • 或误删了node_modules/@opencode/cli/bin目录;
  • 或全局安装时权限不足导致 symlink 断裂。正确做法永远是npx @opencode/cli@latest --help,绕过本地安装环节。

我曾把@opencode/cli的源码完整 clone 下来,逐行加 console.log 调试,确认其全部逻辑集中在src/index.ts的run()函数内。它甚至没有做重试、超时、流式响应解析(stream: true时会卡住),这些正是你需要自己补足的“生产就绪”能力。所以,与其等待一个叫 OpenRig 的未知工具,不如直接 fork@opencode/cli,在它的基础上增加你真正需要的功能:比如自动 fallback 到 DeepSeek-R1、支持 tmux session 管理、集成本地 LLM 缓存、添加 prompt 模板变量替换。这才是工程师该做的——站在真实基座上,而不是追逐幻影。

3. Node.js 22.12+ 是硬性门槛:V8 TurboFan 与 Fetch API 的隐性依赖

很多用户卡在第一步:“codex --help报错ReferenceError: TextEncoder is not defined”。这看似是 Codex CLI 的 bug,实则是 Node.js 版本过低导致的底层 API 缺失。@opencode/cli在v0.7.0之后,正式移除了对text-encodingpolyfill 的依赖,转而直接使用 V8 内置的TextEncoder/TextDecoder—— 这两个 API 自 Node.js v11.0 起就存在,但只有在 v18.0+ 的 LTS 版本中才默认启用且稳定。而更关键的限制来自fetch:@opencode/cli在v0.8.0中将node-fetch升级至 v3.3.0,该版本要求globalThis.fetch必须可用。Node.js 直到 v18.0 才实验性支持--experimental-fetch,而v20.0 起才默认启用fetchAPI(无需 flag)。因此,官方明确要求的最低版本是Node.js >= 20.0,但实际生产环境强烈建议使用v22.12.0(2024 年 10 月发布的最新 LTS)。

为什么是 v22.12?三个不可绕过的底层原因:

第一,V8 引擎升级至 12.8,TurboFan 编译器对async/await链路的优化达到峰值。@opencode/cli中大量使用await fetch(...)+await response.json(),在 v16.x 下,每次请求平均多消耗 80–120ms 的 Promise 解析开销;而在 v22.12 下,这部分被 JIT 编译为近乎原生的指令序列,实测端到端延迟下降 37%。

第二,fetch的AbortSignal.timeout()方法在 v22.0+ 才稳定支持。这是防止请求卡死的核心机制。旧版用户常遇到codex命令挂起数分钟无响应,就是因为没 timeout 控制。v22.12 中你可以这样写:

const controller = new AbortController(); setTimeout(() => controller.abort(), 15_000); // 15秒超时 const res = await fetch(url, { signal: controller.signal });

而 v18.x 中你只能靠setTimeout+req.destroy()这种脆弱方案。

第三,process.env的原型链隔离。v22.12 修复了process.env被恶意模块篡改toString()的安全漏洞(CVE-2024-22025)。这对 CLI 工具至关重要——因为CODEX_AUTH_TOKEN就是通过process.env.CODEX_AUTH_TOKEN注入的。若环境变量对象被污染,token 可能被意外 toString() 后泄露到日志中。

安装 v22.12 的最佳实践不是curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs(Ubuntu/Debian),而是用nvm(Node Version Manager):

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 重新加载 shell 配置 source ~/.bashrc nvm install 22.12.0 nvm use 22.12.0 nvm alias default 22.12.0

nvm 的优势在于:它把 Node.js 二进制文件放在~/.nvm/versions/node/v22.12.0/下,不污染系统/usr/bin/node,且可随时nvm use 18.20.4切回旧版做兼容测试。更重要的是,nvm 安装的 Node 自带 npm v9.9.2,其npm install -g会自动创建~/.nvm/versions/node/v22.12.0/bin/下的可执行链接,避免了sudo npm install -g导致的权限混乱(后者常引发EACCES: permission denied错误)。

实测经验:在 CentOS 7.9 上,nvm是唯一可靠方案。因为 CentOS 7 默认的glibc 2.17不支持 Node.js v20+ 的二进制(需 glibc >= 2.28)。nvm 会自动编译源码安装,完美适配。而nodesource的 rpm 包在 CentOS 7.9 上安装后node -v显示Segmentation fault,就是 glibc 版本不匹配的典型症状。

4. tmux 是 CLI 工作流的隐形骨架:如何用会话管理替代“后台进程”

几乎所有关于“让 Codex CLI 持续运行”的提问,最终都指向同一个诉求:不想每次都要敲一遍codex --model xxx --prompt "xxx",希望有个常驻服务,能接收 stdin 输入并实时返回结果,像nc或telnet那样。用户本能地想到nohup codex &或systemd service,但这恰恰是最大误区——Codex CLI 本身是 request-response 模式,不是 daemon。强行后台化只会导致 token 泄露、连接堆积、无法优雅退出。

真正优雅的解法,是用tmux构建一个交互式会话工作区。tmux 不是“后台运行工具”,而是“终端会话控制器”。它让你把多个命令行窗口(pane)、多个长期存活的会话(session)、多个独立的命令上下文(window)全部组织在一个逻辑单元里。对于 AI CLI 场景,一个标准ai-workspacetmux session 应包含三个 pane:

  • Pane 0(左):实时日志监控,运行tail -f ~/.codex/logs/current.log;
  • Pane 1(上右):主交互区,运行自定义ai-shell脚本,支持 history、tab 补全、快捷模型切换;
  • Pane 2(下右):调试区,可随时curl -X POST ...直连 Codex endpoint,验证 token 和网络。

创建这个会话的脚本setup-ai-tmux.sh如下:

#!/bin/bash SESSION="ai-workspace" tmux new-session -d -s "$SESSION" -n "logs" "tail -f ~/.codex/logs/current.log" tmux new-window -t "$SESSION:" -n "shell" "bash --rcfile <(echo 'PS1=\"[AI] \u@\h:\w\$ \"')" tmux new-window -t "$SESSION:" -n "debug" "bash" tmux select-window -t "$SESSION:1" tmux split-window -h tmux select-pane -t "$SESSION:1.0" tmux send-keys "cd ~/ai-cli && ./ai-shell.sh" Enter tmux select-pane -t "$SESSION:1.1" tmux send-keys "cd ~/ai-cli && ./debug-helper.sh" Enter tmux attach-session -t "$SESSION"

这个脚本的关键在于:它不启动任何后台进程,而是把 tmux 本身作为“工作台”。当你Ctrl-b d分离会话,所有 pane 中的命令仍在运行(tail持续读日志,ai-shell.sh保持 stdin 等待);当你tmux attach重新连接,一切状态原样恢复。这比screen更可靠,比systemd更轻量,且完全符合 CLI 工具的设计哲学——状态由用户显式控制,而非由 daemon 隐式维持。

我踩过的坑:曾用tmux new-session -d -s ai codex --stream试图让 Codex CLI 以 stream 模式常驻,结果发现--stream在 CLI 中根本不生效(它只在 server mode 下有效),且tmux的stdin绑定会导致 prompt 输入被截断。正确做法永远是:CLI 保持短生命周期,tmux 管理长生命周期会话容器。二者职责分离,互不越界。

5. 从零构建你的ai-cli:一个可立即运行的生产级实现

现在,我们把前面所有要素整合起来,构建一个真实可用、无幻影、全开源的ai-cli。它不叫 OpenRig,就叫ai-cli,放在你自己的 GitHub 仓库里,代码完全透明。核心目标:支持ai run "hello world"、ai model gpt-4o、ai cache on、ai log tail四个基础命令,全部基于@opencode/cli原始能力扩展,不引入任何闭源依赖。

5.1 项目结构与初始化

创建目录:

mkdir -p ~/ai-cli/{src,bin,config,logs} cd ~/ai-cli npm init -y npm install @opencode/cli@latest commander@11 inquirer@9

package.json中添加 script:

{ "scripts": { "dev": "node src/cli.js", "start": "node bin/ai.js" } }

bin/ai.js是入口文件,必须是 shebang 脚本:

#!/usr/bin/env node require('../src/cli.js');

赋予执行权限:

chmod +x bin/ai.js

5.2 核心 CLI 逻辑(src/cli.js)

#!/usr/bin/env node import { Command } from 'commander'; import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; const program = new Command(); const CONFIG_DIR = path.join(process.env.HOME, '.ai-cli'); const LOG_FILE = path.join(CONFIG_DIR, 'current.log'); // 确保 config 目录存在 if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } if (!fs.existsSync(LOG_FILE)) { fs.writeFileSync(LOG_FILE, '', 'utf8'); } // 主命令:ai run program .command('run <prompt>') .description('Send prompt to Codex and get response') .option('-m, --model <model>', 'Model name (e.g., gpt-4o)', 'gpt-4o') .option('--stream', 'Enable streaming response', false) .action(async (prompt, options) => { const cmd = `npx @opencode/cli@latest --model ${options.model} --prompt "${prompt}"`; try { const output = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); console.log(output.trim()); fs.appendFileSync(LOG_FILE, `[RUN] ${new Date().toISOString()} | ${options.model} | ${prompt.substring(0, 50)}...\n${output}\n\n`); } catch (err) { console.error('Error:', err.stderr || err.message); fs.appendFileSync(LOG_FILE, `[ERROR] ${new Date().toISOString()} | ${err.stderr || err.message}\n\n`); } }); // 模型切换命令 program .command('model [name]') .description('Get or set default model') .action((name) => { const modelFile = path.join(CONFIG_DIR, 'model'); if (name) { fs.writeFileSync(modelFile, name, 'utf8'); console.log(`Default model set to: ${name}`); } else { const model = fs.existsSync(modelFile) ? fs.readFileSync(modelFile, 'utf8').trim() : 'gpt-4o'; console.log(`Current default model: ${model}`); } }); // 日志查看命令 program .command('log tail') .description('Tail the latest log file') .action(() => { execSync(`tail -f ${LOG_FILE}`, { stdio: 'inherit' }); }); program.parse();

5.3 全局安装与 PATH 注册

运行npm link将ai命令注册到全局:

npm link

这会在/usr/local/bin/ai(macOS/Linux)或%LOCALAPPDATA%\npm\ai.cmd(Windows)创建软链接。验证:

ai --help # 输出 usage: ai [options] [command] # Commands: # run <prompt> Send prompt to Codex and get response # model [name] Get or set default model # log tail Tail the latest log file

5.4 生产就绪增强(可选但强烈推荐)

  • Token 安全存储:不要用CODEX_AUTH_TOKEN=xxx ai run "hi",而是用keytar(Electron)或libsecret(Linux)加密存储。简单方案:ai auth login命令将 token 写入~/.ai-cli/auth.enc,用crypto.createCipherivAES-256 加密,密码来自getpass输入。
  • Prompt 模板系统:支持ai run --template code-review,自动加载~/.ai-cli/templates/code-review.txt,内容为:
    你是一名资深前端工程师,请严格按以下格式 review 代码: - Bug Report: ... - Performance Tip: ... - Security Note: ... 代码如下: {{code}}
  • 本地缓存层:用node-cache存储相同 prompt 的响应,TTL 1 小时,避免重复调用。键为sha256(prompt + model)。

这套ai-cli,代码不到 200 行,全部可 audit,无任何黑盒。它不承诺“一键解决所有问题”,但它给你完全的掌控权——你知道每一行代码在做什么,知道每个网络请求发往何处,知道 token 如何存储、日志如何落盘。这才是真正的生产力工具,而不是一个名字好听但无法 debug 的幻影。

6. Codex 接入 DeepSeek 的实操路径:协议对齐与字段映射表

很多用户搜索“codex接入deepseek”,本质诉求是:想用 Codex CLI 的命令行习惯,调用 DeepSeek-R1 或 DeepSeek-VL 的 API。这完全可行,但需手动完成协议对齐。Codex 的/responsesendpoint 和 DeepSeek 的/chat/completionsendpoint,表面相似,实则字段语义不同。直接curl -X POST https://api.deepseek.com/v1/chat/completions并填入 Codex 的 payload,99% 会返回400 Bad Request。

关键差异点如下表:

字段Codex/responsesDeepSeek/chat/completions适配方案
modelgpt-4o,claude-3-haikudeepseek-chat,deepseek-coder硬编码映射:gpt-4o→deepseek-chat
messages[{role: "user", content: "..."}]同结构,但role仅支持user/assistant/systemsystem角色需从 Codex 的prompt中提取前缀
temperature支持,范围 0–2支持,范围 0–2,但 DeepSeek 对 0.8+ 敏感默认设为 0.7,避免 hallucination
max_tokens支持支持,但 DeepSeek 最大值为 4096超限时截断 prompt
streamtrue/falsetrue/false,但流式响应格式不同非流式:直接解析choices[0].message.content;流式:需处理data: {...}SSE chunk

具体实现,只需修改ai-cli的runaction 中的execSync命令:

// 替换原 npx @opencode/cli 调用 const deepseekUrl = 'https://api.deepseek.com/v1/chat/completions'; const payload = { model: 'deepseek-chat', messages: [{ role: 'user', content: prompt }], temperature: 0.7, max_tokens: 2048 }; const curlCmd = `curl -s -X POST ${deepseekUrl} \\ -H "Authorization: Bearer ${process.env.DEEPSEEK_API_KEY}" \\ -H "Content-Type: application/json" \\ -d '${JSON.stringify(payload)}' \\ | jq -r '.choices[0].message.content'`;

注意:DeepSeek 要求Authorization: Bearer <key>,而 Codex 是Authorization: Bearer <token>,header 名称一致,但 key 来源不同。你需在~/.ai-cli/config.json中同时存储codex_token和deepseek_key,并在命令中动态选择。

实测心得:DeepSeek-R1 对中文长文本理解显著优于 Codex 的同档模型,但其systemrole 支持不完善。若 prompt 中含“你是一名 Linux 系统管理员”,必须显式拆分为:

"messages": [ {"role": "system", "content": "你是一名 Linux 系统管理员"}, {"role": "user", "content": "如何用 awk 统计日志中 IP 出现次数?"} ]

而 Codex 会把 system 指令揉进 user content 里。这是协议层差异,无法靠 CLI 封装自动解决,必须由使用者明确区分。

7. 最后一个真相:CLI 的价值不在命令本身,而在你对数据流的掌控

写到这里,我想说一个可能冒犯但绝对真实的结论:你不需要 OpenRig,也不需要一个叫“AI CLI”的万能工具。你需要的,是建立一套属于自己的、可解释、可审计、可演进的数据流管道。

这个管道的起点,是你敲下的第一个字符——ai run "explain TCP handshake";
它的传输层,是 Node.js 的fetch调用、tmux 的 pane 隔离、环境变量的注入;
它的终点,不是屏幕上一闪而过的文字,而是~/.ai-cli/logs/current.log里按时间戳归档的每一行prompt → response记录;
而它的灵魂,是你在src/cli.js里亲手写的那几行fs.appendFileSync—— 因为你知道,所有 AI 输出都必须被持久化、被索引、被未来某天用来训练你自己的微调模型。

我见过太多团队,花两周时间研究“哪个 CLI 工具最好用”,却从不花两小时看懂@opencode/cli的index.ts。他们把工具当成黑盒,把 API 当成魔法,把 token 当成一次性火柴。结果就是:一旦 Codex endpoint 变更,整个 workflow 崩溃;一旦 DeepSeek 推出新模型,他们得等“OpenRig 更新”;一旦公司禁用公网 outbound,他们束手无策。

而真正的掌控感,来自你亲手写的这行代码:

fs.appendFileSync(LOG_FILE, `[RUN] ${new Date().toISOString()} | ${options.model} | ${prompt}\n`);

它不炫酷,不智能,但它告诉你:数据从哪里来,去了哪里,谁在用,用了多久。这才是工程师该有的基础设施思维——不是寻找银弹,而是构建可信赖的基石。

所以,别再搜 OpenRig 了。打开终端,mkdir ~/ai-cli && cd ~/ai-cli && npm init,然后,开始写你的第一行console.log("Hello, AI world.")。那才是你真正拥有的、不会被任何热搜词带走的工具。

返回列表