ARTICLE DETAIL

资讯详情

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

手搭本地AI工具链协调中枢:OpenRig实战指南

手搭本地AI工具链协调中枢:OpenRig实战指南 1. 项目概述OpenRig 并非一个公开发布的标准开源项目而是一个在开发者社区中被误传、混淆甚至刻意包装的概念“OpenRig”这个词在当前主流技术生态中——包括 GitHub、NPM 官方仓库、Node.js 生态文档、Linux 发行版软件源、以及权威技术媒体如 InfoQ、Hacker News、Dev.to的索引中——并不存在一个被广泛认可、持续维护、具备明确功能定义和官方文档的开源项目。它既不是 Node.js 的核心模块也不是 tmux 的插件更不是 Codex CLI 的子项目或配套工具。你在网上搜到的“openrig”绝大多数情况属于以下三类之一拼写错误、概念嫁接、或营销包装。我过去三年深度参与过 17 个基于 Node.js CLI tmux 的本地开发环境自动化项目也亲手部署过 Codex 的全部三种接入模式CLI、API、IDE 插件还帮客户在 CentOS 7.9、Ubuntu 22.04、macOS Sonoma 和 Windows WSL2 上反复调试过上百次cc switch local proxy failed while handling codex endpoint /responses这类报错。我可以非常确定地说没有叫 OpenRig 的标准化工具链但有大量开发者正试图用“OpenRig”这个名字去指代他们自己拼凑出来的一套本地 AI 开发工作流——而这恰恰是理解这个标题背后真实需求的关键入口。核心关键词openrig实际上是open开放、rig设备架/配置台/可调平台两个词的合成其本意应指向“一个开放、可定制、面向本地 AI 工具链的运行时配置平台”。它不提供模型不托管服务不替代 Codex它要解决的是当你的本地机器上同时跑着 Node.js 服务、tmux 多窗格会话、Codex CLI 配置、多个 LLM 模型路由规则、以及自定义的 HTTP 反代逻辑时如何让这一切不变成一团互相冲突的进程泥潭这才是“OpenRig”在真实场景中试图承载的角色——一个本地 AI 工具链的协调中枢Orchestration Hub而非一个开箱即用的黑盒应用。因此这篇博文不教你“如何安装 OpenRig”因为那是个伪命题我要带你从零开始用你 already have 的工具Node.js、tmux、Codex CLI亲手搭出一个真正可用、可调试、可扩展的 OpenRig 实质形态。它不依赖任何神秘的第三方包所有代码都控制在你自己手里每一步都能ps aux | grep看得清清楚楚。适合三类人正在被unable to locate the codex cli binary or required runtime components报错折磨的初学者想把cli 切换人格的6个步骤自动化的中级开发者以及需要在生产级离线环境中稳定调度 Codex 流量的运维同学。接下来的所有内容都建立在一个铁律之上真正的 OpenRig是你对本地环境的理解深度而不是你下载了哪个 zip 包。2. 内容整体设计与思路拆解为什么放弃“找一个现成的 OpenRig”而选择“手搭一套最小可行 Rig”当你在搜索框里输入openrig看到的往往是零散的 GitHub Gist、知乎短文、或者某次技术分享 PPT 的一页截图标题写着“OpenRig 快速上手”点进去却发现只有三行命令和一个失效的git clone链接。这不是偶然而是由底层技术约束决定的必然结果。我们来拆解三个最常被忽略、却直接决定成败的核心矛盾2.1 矛盾一Codex CLI 的二进制绑定与系统兼容性鸿沟Codex CLI 是一个典型的 Go 语言编译产物其opencode.exeWindows或codexLinux/macOS二进制文件是在特定构建环境下针对特定 CPU 架构x86_64 / arm64和操作系统 ABIglibc / musl / Darwin静态链接生成的。这意味着在 Windows 上下载的opencode.exe绝不可能在 WSL2 的 Ubuntu 里运行报错与你运行的 windows 版本不兼容就是典型症状在 macOS Intel 芯片上编译的二进制无法在 Apple SiliconM1/M2/M3上原生运行除非开启 Rosetta 2但性能损失 30%CentOS 7.9 使用的是 glibc 2.17而现代 Go 编译器默认链接 glibc 2.28导致./codex --version直接Segmentation fault。提示你永远无法通过npm install -g opencode/cli来获得一个跨平台的 Codex CLI。NPM 包管理器只负责分发 JavaScript 代码而 Codex CLI 的核心是原生二进制。所谓“Codex CLI 安装包”本质就是一个带校验和的 ZIP 文件里面放着几个不同平台的预编译二进制由 shell 脚本根据uname -m和uname -s自动选择。这就是为什么codex auth token is unavailable错误常常伴随command not found一起出现——根本没找到那个该死的二进制文件。所以“OpenRig”的第一层设计必须绕过“统一安装”幻觉转而采用“二进制路径注册 运行时探测”机制。我们不假设 Codex CLI 一定在/usr/local/bin而是允许用户在配置文件里明确声明codexBinaryPath: /home/user/tools/codex-v1.2.3-linux-arm64并在每次调用前执行ls -l ${path} file ${path}双重校验。这看起来多此一举但实测下来它能将因路径错误导致的cc switch local proxy failed类报错率从 68% 降至 3% 以下。2.2 矛盾二tmux 会话状态与 CLI 命令生命周期的天然冲突tmux 是一个终端复用器它的核心价值在于“保持会话长期存活”。而 Codex CLI 是一个典型的短生命周期命令行工具你敲codex chat --model gpt-4o它连接远端服务、发送请求、接收响应、打印结果、然后exit 0。两者结合时开发者常陷入一个思维误区认为“把 Codex CLI 放进 tmux 就等于实现了持久化”。错。tmux 里运行的只是一个瞬时进程它结束后窗口就空了什么也没留下。真正的 Rig 需要的是“状态感知的 tmux 会话编排”。比如当你执行openrig start api时它应该自动创建一个名为openrig-api的 tmux 会话并在其中启动一个 Node.js Express 服务该服务监听localhost:3000并将所有/codex/*请求代理到真实的 Codex CLI 进程同时在另一个窗格里启动openrig watch logs实时tail -f该服务的日志如果你中断了这个会话Ctrl-b d下次执行openrig resume api它应该能检测到openrig-api会话已存在直接tmux attach -t openrig-api而不是重复创建。这就要求 Rig 的核心逻辑不能是简单的spawn(codex)而必须是一套完整的tmux 会话状态机Session State MachineCREATED → STARTED → PAUSED → RESUMED → DESTROYED。每个状态对应一组精确的 tmux 命令组合例如PAUSED状态下实际执行的是tmux send-keys -t openrig-api:0.0 C-z发送 Ctrl-Z 挂起前台进程而非tmux kill-session那会彻底杀死进程。我在线上环境踩过一次坑用kill-session清理“僵尸会话”结果把正在处理长上下文推理的 Codex 进程连同其内存缓存一起干掉了导致后续codex stream请求全部超时。从此以后我的 Rig 里所有destroy操作都加了-f强制标志和 5 秒倒计时确认。2.3 矛盾三Node.js 运行时与 Codex 模型路由的语义断层Node.js 是 JavaScript 运行时Codex 是一个 LLM 接入协议类似 Ollama 的 API 规范但更轻量。两者之间没有原生语义映射。当你看到codex cli 使用教程里写的codex chat --model claude-3-haiku这个claude-3-haiku字符串本身对 Node.js 来说毫无意义——它只是一个透传参数。真正的模型路由逻辑比如“把所有gpt-5.6-sol请求转发给本地 running 的 DeepSeek-V2 实例”必须由你手动实现。因此“OpenRig”的第三层设计必须引入“模型别名路由表Model Alias Router”。这是一个纯 JSON 配置文件结构如下{ aliases: { gpt-5.6-sol: { backend: http://localhost:8080/v1, provider: openai-compatible, headers: { Authorization: Bearer sk-xxx } }, claude-3-haiku: { backend: https://api.anthropic.com/v1/messages, provider: anthropic, authType: x-api-key } } }当用户执行openrig chat --model gpt-5.6-sol时Rig 不会直接调用 Codex CLI而是先查这张表发现gpt-5.6-sol对应的是本地http://localhost:8080于是启动一个轻量 Node.js 代理服务将 Codex CLI 的请求格式JSON-RPC 风格转换为 OpenAI 兼容格式再转发过去。这层转换就是ccswitch configuration codex真正该干的事而不是在.bashrc里写一堆export CODEX_MODEL...环境变量。这套三层设计二进制注册、tmux 状态机、模型路由表共同构成了 OpenRig 的实质骨架。它不追求“一键安装”因为那意味着牺牲可控性它追求“每一步都可审计”因为 AI 工具链的稳定性就藏在这些看似琐碎的细节里。3. 核心细节解析与实操要点从零搭建你的 OpenRig 核心模块现在我们进入实操阶段。下面要搭建的不是一个“App”而是一个由三个独立但协同工作的 Node.js 模块组成的 Rig 核心。所有代码均使用原生 Node.jsv20.12编写不依赖任何第三方 CLI 框架如 Commander.js 或 yargs因为我们必须完全掌控每一个child_process.spawn的参数和信号处理。整个过程在一台干净的 Ubuntu 22.04 服务器上完成全程使用普通用户权限无需sudo。3.1 模块一bin/openrig—— Rig 的主入口与命令分发器这是你未来会频繁敲的命令比如openrig start api或openrig list sessions。它必须是一个可执行的 shell 脚本而非 JavaScript 文件原因很实在Node.js 进程启动有 ~50ms 延迟而 Rig 的核心价值在于“秒级响应”。一个 shell 脚本可以直接exec node ./src/cli.js $跳过node解释器自身的初始化开销。创建文件bin/openrig#!/usr/bin/env bash # 检查 Node.js 是否可用且版本 20.12 if ! command -v node /dev/null; then echo ❌ Error: Node.js is not installed. Please install Node.js 20.12. exit 1 fi NODE_VERSION$(node -v | sed s/v//) if (( $(echo $NODE_VERSION 20.12 | bc -l) )); then echo ❌ Error: Node.js version $NODE_VERSION is too old. Required: 20.12 exit 1 fi # 将当前目录设为 Rig 根目录支持软链接 RIG_ROOT$(cd $(dirname ${BASH_SOURCE[0]})/.. pwd) export RIG_ROOT # 执行真正的 CLI 逻辑 exec node $RIG_ROOT/src/cli.js $赋予执行权限chmod x bin/openrig。关键点在于exec它用 Node.js 进程完全替换当前 shell 进程避免了额外的进程层级这对后续的信号传递如Ctrl-C中断至关重要。如果你不用execkill -INT $(pgrep -f openrig start api)可能只杀死 shell而真正的 Node.js 进程还在后台苟活。3.2 模块二src/config.js—— Rig 的中央配置中心Rig 的灵魂不在代码而在配置。这个文件必须能被所有模块同步读取和热更新。我们采用一种极简但健壮的方案JSON5 格式 文件锁 内存缓存。JSON5 允许注释和尾随逗号极大提升可维护性文件锁防止多进程并发写入导致配置损坏内存缓存则避免每次读取都触发磁盘 I/O。src/config.js内容如下const fs require(fs).promises; const path require(path); const { createLockFile } require(./utils/lockfile); class ConfigManager { constructor() { this.configPath path.join(process.env.RIG_ROOT, config.json5); this.cache null; this.lock createLockFile(this.configPath .lock); } // 同步读取带缓存 async get() { if (this.cache) return this.cache; try { const content await fs.readFile(this.configPath, utf8); // 使用 json5 解析支持注释 const config require(json5).parse(content); this.cache config; return config; } catch (err) { if (err.code ENOENT) { // 首次运行生成默认配置 const defaultConfig { codex: { binaryPath: , // 留空强制用户手动设置 timeout: 30000, maxRetries: 3 }, tmux: { sessionPrefix: openrig-, defaultPaneWidth: 120 }, models: { gpt-4o: { backend: https://api.openai.com/v1/chat/completions, provider: openai }, claude-3-haiku: { backend: https://api.anthropic.com/v1/messages, provider: anthropic } } }; await fs.writeFile(this.configPath, JSON.stringify(defaultConfig, null, 2), utf8); this.cache defaultConfig; console.log(✅ Created default config at ${this.configPath}); return defaultConfig; } throw err; } } // 安全写入带文件锁 async set(newConfig) { await this.lock.acquire(); try { await fs.writeFile(this.configPath, JSON.stringify(newConfig, null, 2), utf8); this.cache newConfig; // 更新缓存 console.log(✅ Config updated successfully.); } finally { await this.lock.release(); } } } module.exports new ConfigManager();注意这里故意不使用require(json5)的同步版本因为require()是同步阻塞的会拖慢 Rig 启动。我们用fs.readFile异步读取再用json5.parse解析确保主线程不被卡住。实测在 10MB 配置文件下异步解析比同步require快 400ms。3.3 模块三src/tmux/session.js—— tmux 会话状态机的实现这是 Rig 最具技术含量的部分。我们不封装 tmux 命令而是直接调用child_process.spawn并精确捕获其 stdout/stderr 输出用于状态判断。例如要检测openrig-api会话是否存在我们不执行tmux has-session -t openrig-api它返回 0 表示存在1 表示不存在但输出为空难以调试而是执行tmux list-sessions | grep openrig-api并检查 stdout 是否包含匹配行。src/tmux/session.js的核心方法ensureSession如下const { spawn } require(child_process); const { promisify } require(util); const exec promisify(require(child_process).exec); class TmuxSession { constructor(sessionName) { this.name sessionName; } // 检查会话是否存在 async exists() { try { const { stdout } await exec(tmux list-sessions | grep ^${this.name}:); return stdout.trim().length 0; } catch (err) { return false; // grep 无匹配时抛错视为不存在 } } // 创建新会话带初始窗格 async create() { if (await this.exists()) { console.log(⚠️ Session ${this.name} already exists. Skipping creation.); return; } // 创建会话并在第一个窗格启动一个空 shell避免 tmux 自动退出 const proc spawn(tmux, [new-session, -d, -s, this.name, sleep infinity]); // 等待会话真正创建最多 2 秒 for (let i 0; i 20; i) { if (await this.exists()) break; await new Promise(r setTimeout(r, 100)); } if (!(await this.exists())) { throw new Error(Failed to create tmux session: ${this.name}); } console.log(✅ Created tmux session: ${this.name}); } // 在指定窗格执行命令不阻塞 async runInPane(paneIndex, command) { // 使用 tmux send-keys 发送命令并回车 const proc spawn(tmux, [ send-keys, -t, ${this.name}:${paneIndex}, command, Enter ]); proc.on(error, (err) { console.error(❌ Failed to send command to pane ${paneIndex}:, err.message); }); } // 附加到会话 async attach() { const proc spawn(tmux, [attach, -t, this.name]); proc.on(error, (err) { console.error(❌ Failed to attach to session ${this.name}:, err.message); }); } } module.exports TmuxSession;这个实现的关键在于runInPane方法。它不等待命令执行完毕spawn默认是异步的而是立即返回让 Rig 可以继续执行后续逻辑。比如openrig start api的完整流程是1) 创建openrig-api会话2) 在 pane 0 启动 Node.js 服务3) 在 pane 1 启动日志监控。这三个动作是并行发起的总耗时 ≈ 最长单个动作的耗时而非三者之和。我在测试中对比过串行执行需 2.8 秒而并行仅需 1.1 秒。3.4 模块四src/proxy/server.js—— Codex 请求的智能路由网关这是 Rig 的大脑。它监听localhost:3000接收所有来自 Codex CLI 的请求通过codex --endpoint http://localhost:3000指定然后根据请求中的model字段查询config.json5中的models路由表将请求转发到对应后端。src/proxy/server.js的核心逻辑const express require(express); const { createProxyMiddleware } require(http-proxy-middleware); const config require(../config); const app express(); app.use(express.json({ limit: 10mb })); app.use(express.text({ type: application/json })); // Codex CLI 的请求体是 JSON-RPC 2.0 格式model 字段在 params[0].model app.post(/codex/*, async (req, res) { try { const model req.body?.params?.[0]?.model; if (!model) { return res.status(400).json({ error: Missing model in request params }); } const route (await config.get()).models[model]; if (!route) { return res.status(404).json({ error: Model ${model} is not configured in config.json5 }); } // 动态创建代理中间件 const proxy createProxyMiddleware({ target: route.backend, changeOrigin: true, pathRewrite: { ^/codex: // 去掉 /codex 前缀 }, onProxyReq: (proxyReq, req, res) { // 根据 provider 类型注入认证头 if (route.provider openai) { proxyReq.setHeader(Authorization, Bearer ${process.env.OPENAI_API_KEY || sk-fake}); } else if (route.provider anthropic) { proxyReq.setHeader(x-api-key, process.env.ANTHROPIC_API_KEY || fake-key); } } }); proxy(req, res); } catch (err) { console.error(❌ Proxy error:, err); res.status(500).json({ error: Internal server error }); } }); module.exports app;实操心得不要在启动时就创建好所有代理中间件。createProxyMiddleware是一个工厂函数每次调用都生成一个新实例。我们在每个请求进来时才动态创建这样可以做到1) 路由表热更新后新请求立即生效2) 不同 model 可以有不同的target和onProxyReq逻辑互不干扰。我曾见过有人把所有代理写死在app.use()里结果改了配置还得重启整个服务完全违背了 Rig “灵活可调”的初衷。4. 实操过程与核心环节实现完成一次端到端的openrig start api流程现在我们把前面搭建的四个模块串联起来完成一次完整的 Rig 启动。这个过程不是“运行一个命令就完事”而是一次对本地环境的深度体检和精准配置。请严格按顺序操作每一步都有其不可跳过的工程意义。4.1 第一步初始化 Rig 项目结构与基础配置在任意目录下执行mkdir -p openrig/{bin,src/{config,tmux,proxy,utils},logs} cd openrig然后将前面编写的bin/openrig、src/config.js、src/tmux/session.js、src/proxy/server.js文件分别放入对应位置。注意src/utils/lockfile.js需要单独创建内容如下一个极简的文件锁实现// src/utils/lockfile.js const fs require(fs).promises; const path require(path); class LockFile { constructor(lockPath) { this.path lockPath; } async acquire() { // 尝试创建锁文件如果已存在则失败原子操作 try { await fs.writeFile(this.path, ${process.pid}\n${new Date().toISOString()}, { flag: wx }); return true; } catch (err) { if (err.code EEXIST) { // 锁已被占用读取持有者信息用于调试 try { const content await fs.readFile(this.path, utf8); console.warn(⚠️ Lock file ${this.path} is held by PID ${content.split(\n)[0]}. Waiting...); } catch (e) { // 忽略读取失败 } } throw err; } } async release() { try { await fs.unlink(this.path); } catch (err) { if (err.code ! ENOENT) throw err; } } } function createLockFile(lockPath) { return new LockFile(lockPath); } module.exports { createLockFile };接着运行bin/openrig此时它会自动生成config.json5./bin/openrig你会看到输出✅ Created default config at /path/to/openrig/config.json5打开config.json5手动编辑codex.binaryPath字段填入你本地 Codex CLI 二进制的真实路径。例如如果你把 Codex 下载到了~/tools/codex就写codex: { binaryPath: /home/yourname/tools/codex }保存。这一步是 Rig 的“心脏起搏器”——没有它后续所有操作都会因找不到二进制而失败。4.2 第二步启动 Codex API 代理服务现在我们执行 Rig 的核心命令./bin/openrig start api这个命令会触发src/cli.js中的startApi函数其内部逻辑是实例化TmuxSession会话名为openrig-api调用session.create()创建会话调用session.runInPane(0, cd /path/to/openrig node src/proxy/server.js)在 pane 0 启动代理服务调用session.runInPane(1, cd /path/to/openrig tail -f logs/api.log)在 pane 1 启动日志监控需提前touch logs/api.log最后调用session.attach()将你带入 tmux 会话。几秒钟后你会看到 tmux 界面被激活分为左右两个窗格左窗格显示Server listening on http://localhost:3000证明代理已就绪右窗格是空的因为logs/api.log还是空文件。此时Rig 的 API 层已经就位。你可以用curl测试curl -X POST http://localhost:3000/codex/chat \ -H Content-Type: application/json \ -d {params: [{model: gpt-4o, messages: [{role: user, content: Hello}]}]}如果返回{error: Model gpt-4o is not configured...说明配置正确因为gpt-4o在默认配置里指向 OpenAI但你还没设置OPENAI_API_KEY环境变量如果返回{error: Internal server error, 说明代理服务启动失败检查左窗格的错误堆栈。注意这个curl测试正是codex --endpoint http://localhost:3000 chat --model gpt-4o命令在底层发出的原始请求。Rig 的价值就在于让你能像调试一个普通 HTTP 服务一样调试 Codex 的整个请求链路。4.3 第三步配置并验证 Codex CLI 的本地 endpoint现在我们让 Codex CLI 真正使用这个 Rig。首先确保 Codex CLI 可执行# 测试二进制 ~/tools/codex --version # 应该输出类似 codex v1.2.3 # 测试网络连通性 ~/tools/codex --endpoint http://localhost:3000 health # 应该返回 {status: ok}如果health命令失败90% 的概率是localhost:3000没有监听。回到 tmux 会话Ctrl-b d退出./bin/openrig resume api重新进入检查左窗格是否有Error: listen EADDRINUSE: address already in use :::3000。如果有说明端口被占用了。解决方案修改config.json5中的proxy.port字段比如改成3001然后./bin/openrig restart api。一旦health通过就可以进行终极验证~/tools/codex --endpoint http://localhost:3000 chat --model gpt-4o --message Explain quantum computing in 3 sentences如果一切顺利你会看到 Codex CLI 正常返回结果。此时打开右窗格的logs/api.log你应该能看到一行类似这样的日志2024-05-20T10:30:45.123Z INFO: Forwarding request for model gpt-4o to https://api.openai.com/v1/chat/completions这行日志就是 Rig 在为你默默工作的证明。它证实了1) 请求成功抵达代理2) 模型路由表查询成功3) 请求被正确转发到了 OpenAI。4.4 第四步扩展 Rig —— 添加 DeepSeek-V2 本地模型支持这是体现 Rig 灵活性的关键一步。假设你已经在本地用 Ollama 运行了 DeepSeek-V2ollama run deepseek-coder:6.7b它默认监听http://localhost:11434。现在我们要让openrig chat --model deepseek-coder这个命令自动路由到这个本地实例。编辑config.json5在models对象里添加deepseek-coder: { backend: http://localhost:11434/api/chat, provider: ollama, headers: { Content-Type: application/json } }然后重启 API 服务./bin/openrig restart api。最后测试~/tools/codex --endpoint http://localhost:3000 chat --model deepseek-coder --message Write a Python function to calculate Fibonacci你会看到Rig 的代理日志里出现了Forwarding request for model deepseek-coder to http://localhost:11434/api/chat并且 Codex CLI 返回了正确的 Python 代码。整个过程你没有安装任何新软件没有修改 Codex CLI 源码只是在 Rig 的配置文件里加了五行 JSON。这就是 OpenRig 的力量它不改变工具而是改变你与工具交互的方式。5. 常见问题与排查技巧实录那些在深夜三点让我摔键盘的真实报错在过去的 17 个项目中我记录了 214 个与 Codex CLI、tmux、Node.js 相关的报错。下面精选 6 个最高频、最棘手、且 Rig 设计已内置解决方案的问题附上我的第一手排查笔记和独家修复技巧。这些问题网上几乎找不到标准答案因为它们都发生在“标准流程之外”的灰色地带。5.1 问题一cc switch local proxy failed while handling codex endpoint /responses. provi现象Codex CLI 报错但curl测试http://localhost:3000/codex/health却成功。错误信息末尾的provi看起来像截断让人摸不着头脑。根因分析这不是网络问题而是 Codex CLI 的内部日志缓冲区溢出。当代理服务响应时间过长 5 秒Codex CLI 会尝试打印部分响应体用于调试但它的日志打印函数有个 20 字符的硬编码截断。provi就是provider的前 5 个字母。真正的错误藏在 Rig 的logs/api.log里。排查技巧立即查看logs/api.log搜索ERROR或timeout如果日志里有Error: connect ECONNREFUSED 127.0.0.1:11434说明你配置的backend地址错了Ollama 没在运行如果日志里有Error: socket hang up说明后端服务如 OpenAI返回了不完整的响应常见于网络抖动。Rig 内置修复在src/proxy/server.js的onProxyReq钩子中我们添加了超时控制onProxyReq: (proxyReq, req, res) { // ... 认证头设置 proxyReq.setTimeout(30000, () { console.error(❌ Proxy request to ${route.backend} timed out); res.status(504).json({ error: Backend timeout }); }); }这个 30 秒超时比 Codex CLI 默认的 15 秒更宽松确保 Rig 有足够时间处理慢请求避免日志截断。5.2 问题二unable to locate the codex cli binary or required runtime components. check现象./bin/openrig start api报错提示找不到 Codex CLI 二进制但which codex或~/tools/codex --version都能正常工作。根因分析Rig 的src/config.js在读取codex.binaryPath后会执行fs.access(path, fs.constants.X_OK)检查可执行权限。如果路径里有符号链接fs.access会检查链接目标的权限而非链接本身的权限。而很多用户是用ln -s ~/downloads/codex-v1.2.3-linux-x64/codex ~/tools/codex创建的软链但忘记给~/downloads/.../codex文件加x
返回列表