1. openrig 到底想解决什么问题第一次看到openrig这个词很多人会以为是某个硬件机架项目或者又是一个新的前端脚手架。但把它的关键词摊开看——Claude Code、Codex、Node.js、tmux——方向就很清楚了它瞄准的是多 AI 编码代理coding agent的本地编排与终端复用这件事。我先说说这个需求是怎么冒出来的。现在用 Claude Code 或者 Codex CLI 写代码的人越来越多但几乎所有人都会撞上同一堵墙你只有一个终端窗口一次只能跑一个代理会话。想同时让一个代理去重构模块 A另一个代理去补模块 B 的测试第三个代理在后台跑长任务怎么办大多数人的做法是开三个终端标签页然后手动切来切去切到最后自己都不知道哪个窗口在干什么。更麻烦的是代理跑到一半要你确认权限、要你输入一段上下文而你正在另一个窗口里看日志等回过头来那个会话已经超时挂掉了。openrig要处理的就是这种多个代理会话并行 统一调度 会话不丢的场景。它本质上是一层架在 Claude Code、Codex 这类 CLI 代理之上的会话管理层底层靠 Node.js 做进程与事件编排靠 tmux 做终端会话的持久化与复用。你可以把它理解成一个代理机架——每个代理是一个插槽你负责往里塞任务它负责让这些插槽互不干扰地跑下去。适合谁看这篇内容三类人最对口。第一类是本机已经装了 Claude Code 或 Codex日常靠命令行干活但被单窗口限制卡住的开发者。第二类是想把 AI 代理接进自己工作流、做自动化编排的工程师比如让代理定时跑代码审查、跑依赖升级。第三类是对 Node.js 进程管理和 tmux 会话机制感兴趣、想搞明白代理在后台到底怎么活着的技术人。如果你只是偶尔用网页版问几个问题那这篇对你价值不大但只要你的代理会话超过两个下面的内容就能直接抄。需要先说明一点openrig这个项目本身在公开渠道的资料非常少输入里给的项目正文和关键词都是空的所以我不会去编造它的具体 API 或源码结构。下面所有关于它怎么做的部分都是基于这类工具在真实工程里最合理的实现路径来展开的我会明确标注哪些是通用实践、哪些是需要你按自己环境调整的部分。这样你拿到的不是一份虚构的文档而是一套能落地的方法论。2. 为什么是 Node.js 加 tmux 这套组合2.1 Node.js 在代理编排里扮演的角色很多人会问编排几个终端会话而已用 bash 脚本不就够了为什么要上 Node.js这个问题我认真想过答案在于事件模型。bash 脚本是线性的你写一串命令它从上往下执行中间某个代理要等你输入整个脚本就卡在那里。而 AI 代理会话的本质是长生命周期 异步事件——代理可能在任意时刻输出一段内容、请求一次权限、抛出一个错误、或者干脆静默几分钟。你要管理 N 个这样的会话就必须有一个能同时监听 N 个事件源、并且能在事件之间做状态流转的运行时。Node.js 的事件循环天生就是干这个的child_process模块能拿到子进程的 stdout/stderr 流EventEmitter能把每个代理的输出变成可订阅的事件这套东西用 bash 写会非常痛苦。具体到实现层面一个代理会话在 Node.js 里通常是这样被托管的主进程用spawn拉起一个 tmux 会话tmux 里再跑 Claude Code 或 Codex 的 CLI。主进程不直接读代理的 stdout而是通过tmux capture-pane或者tmux pipe-pane把终端内容捞出来解析成结构化事件。这样做的好处是代理进程和你的主控进程解耦——主控崩了tmux 里的代理还活着你关掉终端代理照样在后台跑。这就是为什么 tmux 是这套方案里不可替代的一环。提示Node.js 版本建议用 LTS。热词里出现过node.js v24.21.0 is not yet released这类报错本质是版本号写错了或者源里没有这个版本。装 Node.js 认准官网的 LTS 下载页别去追奇数版本号。2.2 tmux 提供的会话不死能力tmux 的核心价值就一句话它让终端进程脱离了你的登录会话。你 SSH 断开、关掉终端窗口、甚至网络抖一下tmux 里的进程都还在跑。对于动辄要跑十几分钟的代理任务这一点是刚需。在 openrig 这类工具里tmux 通常承担三件事。第一是会话隔离每个代理一个独立 session命名规范一般是openrig-agent-taskid这种方便你按前缀批量操作。第二是输出捕获通过capture-pane -p把当前屏幕内容抓成纯文本喂给上层做解析。第三是输入注入用send-keys把用户的指令或者预设的确认输入打进会话里实现无人值守。这里有个实操细节值得说capture-pane默认抓的是可视区域如果代理输出很长你只能拿到最后一屏。要拿完整历史得加-S -参数从会话开始抓。但抓全量历史在长会话里会非常慢所以成熟的做法是增量抓取——记录上次抓到的行号每次只抓新增部分。这个优化不做代理跑一小时后你的主控进程光解析输出就能卡死。2.3 两者结合后的架构轮廓把上面两块拼起来openrig 的架构轮廓大致是这样Node.js 主进程维护一个代理注册表每个条目记录 tmux session 名、代理类型Claude Code / Codex、当前状态idle / running / waiting-input / done、最后活动时间。主进程通过定时轮询加事件监听的方式从各个 tmux 会话里拉取输出做状态机流转。当某个代理进入waiting-input状态主进程可以按预设规则自动应答也可以推送到你的界面让你手动处理。这套架构的关键设计取舍在于轮询还是事件驱动。纯事件驱动pipe-pane 写文件 fs.watch延迟低但实现复杂容易漏事件纯轮询定时 capture-pane实现简单但有延迟、有开销。实际项目里常见的是混合模式pipe-pane 负责把输出落盘保证不丢轮询负责定期同步状态两者互补。这个取舍没有标准答案取决于你对延迟的容忍度和代理数量。3. 把 Claude Code 和 Codex 接进同一套编排3.1 两个 CLI 代理的行为差异Claude Code 和 Codex CLI 虽然都是命令行里的 AI 编码代理但它们的行为模式差别不小这直接决定了编排层要怎么写。Claude Code 的交互更偏向对话式它会频繁地请求确认比如我要执行这个命令可以吗输出格式相对稳定有比较清晰的提示符。Codex CLI 则更偏向任务式你给它一个任务它闷头跑中间的输出节奏和 Claude Code 不一样。热词里那条codex is ignoring 1 unrecognized configuration setting就是典型的 Codex 配置解析行为——它对不认识的配置项是警告而非报错这个特性在编排时要注意别把警告当成失败。维度Claude CodeCodex CLI交互风格对话式频繁确认任务式较少打断配置容错相对严格未知配置项仅警告会话恢复支持继续上次会话依赖具体版本输出节奏稳定、有提示符突发式、成块输出这张表不是让你背而是提醒你编排层不能假设两个代理行为一致。你的状态机要能识别这个代理在等输入和这个代理只是在慢慢思考的区别否则会误判、误注入。3.2 统一会话抽象的设计思路要让 openrig 同时管住两个不同的代理第一步是抽出一个统一的会话接口。我见过比较靠谱的做法是定义一个AgentSession抽象包含这些方法start()拉起 tmux 会话并启动对应 CLIsend(input)注入输入capture()抓取当前输出status()返回归一化后的状态stop()优雅关闭。关键在于status()的归一化。Claude Code 和 Codex 的原始输出格式不同但你可以把它们都映射到同一组状态IDLE空闲待命、THINKING正在生成、WAITING_CONFIRM等待用户确认、ERROR出错、DONE任务完成。映射规则靠模式匹配实现——比如检测到 Claude Code 的确认提示符就判为WAITING_CONFIRM检测到 Codex 的完成标记就判为DONE。这里必须提醒一个坑模式匹配极其脆弱。代理 CLI 一升级提示符文案变了你的匹配规则就全废。所以成熟项目会把匹配规则做成可配置的外部文件而不是硬编码在代码里。CLI 升级后你只需要改配置不用重新发版。这个设计决策在项目初期很容易被忽略等到第一次 CLI 大版本更新时就会付出代价。3.3 权限确认的自动应答策略代理最烦人的地方就是频繁要权限。在单窗口场景下你手动点一下无所谓但在多代理并行场景下一个代理卡在权限确认上整个任务链就断了。所以自动应答是编排层的核心能力。策略上分三档。全自动预设规则凡是匹配到的确认一律回 yes适合你完全信任的只读任务比如代码分析、文档生成。白名单自动只有特定类型的操作自动放行比如读文件、跑测试涉及写文件、执行任意命令的还是要人工。全手动所有确认都推给你适合高风险操作。我的经验是默认用白名单模式全自动只在沙箱环境或者你非常确定任务边界时用。因为代理的确认请求有时候措辞很模糊你以为它在问能不能读这个文件实际上它在问能不能覆盖这个文件全自动放行会出事。这个判断没有捷径只能靠你对具体代理行为的熟悉程度。4. 从零搭一套可用的本地编排环境4.1 环境准备里最容易被忽略的三件事装 Node.js、装 tmux、装 Claude Code 或 Codex这些步骤网上一搜一大把我不重复。我说三个真正容易翻车的地方。第一tmux 的默认配置会坑你。默认情况下 tmux 的历史缓冲区只有 2000 行代理跑久了输出被截断你的 capture 就拿不到完整内容。改法是在~/.tmux.conf里加set-option -g history-limit 50000然后重启 tmux server。这个数字按你的内存给50000 行大概占几十 MB一般机器扛得住。第二Node.js 的 spawn 和 shell 转义。你用spawn拉起 tmux 命令时如果参数里带空格或者特殊字符很容易被 shell 拆错。稳妥做法是永远用数组形式传参不要拼字符串并且shell: false。我见过太多因为路径里有空格导致会话起不来的案例。第三代理 CLI 的登录态。Claude Code 和 Codex 都需要登录而登录态通常存在用户目录下。如果你用 tmux 起会话时环境变量不对比如HOME被改了代理会认为你没登录直接报错退出。热词里那条your organization has disabled claude subscription access和claude code might not be available in your country都属于这类环境/账号问题排查时先确认echo $HOME和登录态文件路径对不对。4.2 用 tmux 起一个受控代理会话下面这段是起一个受控 Claude Code 会话的最小示例你可以直接改成 Codex 版本# 创建一个独立命名的 tmux 会话detached 模式后台运行 tmux new-session -d -s openrig-claude-001 -x 220 -y 50 # 在会话里启动 Claude Code假设已全局安装 tmux send-keys -t openrig-claude-001 claude Enter # 等它初始化完抓一次屏幕确认状态 sleep 3 tmux capture-pane -t openrig-claude-001 -p几个参数解释一下。-x 220 -y 50是强制指定终端尺寸这很重要——tmux 默认尺寸可能很小代理的 TUI 界面在小尺寸下会换行错乱导致你的模式匹配失效。固定一个足够大的尺寸输出才稳定。-d是 detached会话在后台起不占用你当前终端。抓屏那一步是验证会话是否正常起来的关键。如果capture-pane出来是空白或者报错说明代理没启动成功常见原因是 PATH 里找不到claude命令。tmux 会话的环境变量继承自你创建会话时的 shell如果你在非交互 shell 里创建PATH 可能不全。解决办法是在send-keys之前先send-keys export PATH... Enter或者干脆用绝对路径启动。4.3 Node.js 侧的会话管理骨架光有 tmux 命令还不够你需要一个 Node.js 层来统一管理。下面是一个简化但可用的骨架const { execFile } require(child_process); const { promisify } require(util); const execFileAsync promisify(execFile); class TmuxSession { constructor(name) { this.name name; } async create() { await execFileAsync(tmux, [ new-session, -d, -s, this.name, -x, 220, -y, 50 ]); } async send(keys) { await execFileAsync(tmux, [send-keys, -t, this.name, keys, Enter]); } async capture() { const { stdout } await execFileAsync(tmux, [ capture-pane, -t, this.name, -p, -S, -200 ]); return stdout; } async kill() { await execFileAsync(tmux, [kill-session, -t, this.name]); } }这段代码的重点不在功能而在错误处理的位置。真实项目里execFileAsync的每一次调用都要包 try/catch因为 tmux 会话可能在你操作的一瞬间被外部关掉了这时候命令会抛错。如果你不处理整个主控进程就崩了。我建议给每个会话操作加重试 超时比如 capture 超过 2 秒没返回就跳过这一轮下一轮再试。还有一个细节capture-pane的-S -200表示从当前往上抓 200 行。这个数字要和你 tmux 的 history-limit 配合抓取范围不能超过缓冲区大小否则拿到的还是截断内容。5. 多代理并行时那些真正会咬人的坑5.1 输出解析的误判与漏判多代理并行时最消耗精力的不是启动会话而是判断每个代理现在到底在干嘛。我踩过最典型的一个坑是代理在生成一大段代码中间停顿了几秒我的状态机因为几秒没收到新输出就把它判成了IDLE然后自动注入了一条新指令结果把代理正在生成的代码打断了。这个问题的根因是用有没有输出来判断在不在工作是错的。代理思考时可能长时间静默代理等待时也可能有光标闪烁这种视觉噪音。正确的做法是结合多个信号输出内容里有没有明确的完成标记、有没有确认提示符、进程的 CPU 占用、最后活动时间。单一信号一定会误判。漏判则更隐蔽。代理输出里包含了你没预料到的提示符变体你的匹配规则没覆盖于是状态一直停在THINKING任务实际上早就完成了。解决办法是定期做一次全量状态校准比如每 30 秒强制抓一次完整屏幕用更宽松的规则重新判定一次状态纠正增量判断的偏差。5.2 会话命名冲突与资源泄漏代理跑多了会话名冲突是迟早的事。如果你用时间戳命名同一秒内起两个会话就撞了如果你用递增 ID进程重启后 ID 归零又会撞上残留的旧会话。我的做法是命名里带一个随机短串比如openrig-claude-001-a3f2冲突概率极低。比命名冲突更严重的是资源泄漏。代理任务结束后如果你忘了kill-sessiontmux 会话会一直挂着占内存、占进程数。跑一天下来几十个僵尸会话机器就卡了。所以你的编排层必须有会话回收机制任务标记为DONE后延迟一段时间自动 kill或者启动时先扫描一遍所有openrig-前缀的会话把超过一定时长没活动的清理掉。注意清理会话前一定要确认代理真的结束了别把正在跑长任务的会话误杀。稳妥做法是清理前先 capture 一次确认状态是DONE或ERROR再动手。5.3 代理卡死时的排查链路代理卡死是家常便饭排查要有一套固定链路别东一榔头西一棒子。我的顺序是这样的先看 tmux 会话还在不在tmux ls如果会话没了说明代理进程崩了去看有没有 core dump 或者错误日志。再看会话里最后几屏内容tmux capture-pane -t name -p -S -50通常能看到报错信息或者卡在哪个提示符上。确认是不是在等输入如果最后一行是确认提示符那就是卡在权限确认手动send-keys回一个就行。检查网络和 API 状态代理要调远端模型网络断了或者 API 限流都会导致长时间无响应。热词里cc switch local proxy failed while handling codex endpoint这类报错就是代理层和模型服务之间的连接问题。最后才考虑重启会话前面都排除了再 kill 掉重来并且把这次的输出日志留下来分析。这套链路的价值在于从外到内、从便宜到贵。先做成本最低的检查会话在不在再做需要读日志的检查最后才做代价最高的重启。很多人一上来就重启结果同样的问题反复出现因为根因从来没被找到。6. 把本地模型和第三方接口接进来的注意事项6.1 本地模型接入的延迟特性热词里有个很具体的需求claude code 调用 lmstudio 的本地模型。把代理指向本地模型是很多人的诉求省钱、隐私好、不怕限流。但本地模型有个绕不开的特性首 token 延迟高吞吐不稳定。云端模型通常几百毫秒就出第一个 token本地模型在消费级显卡上可能要几秒甚至十几秒。这个延迟对你的编排层是致命的——你的状态机如果按云端模型的节奏设超时本地模型会被你反复误判成卡死。所以接入本地模型时所有超时参数都要放大确认提示的等待时间、状态校准的间隔、任务超时阈值统统往上调。另一个坑是上下文长度。本地模型的上下文窗口通常比云端小代理跑长任务时容易超限。你需要在编排层加一个上下文监控接近上限时主动提醒或者触发会话重启。这个功能不做代理跑到一半突然报上下文超限前面的工作全白费。6.2 第三方接口的配置隔离用第三方 API 接入代理时配置管理是个大问题。热词里codex is ignoring 1 unrecognized configuration setting和codex无法加载组织设置都指向同一类问题配置项写错或者放错位置代理要么忽略要么报错。我的建议是每个代理实例一套独立配置不要共用全局配置。因为不同任务可能要用不同的模型、不同的 API 端点、不同的参数共用配置会导致互相污染。实现上可以给每个 tmux 会话设置独立的环境变量代理启动时读环境变量而不是读全局配置文件。这样隔离干净也方便你做 A/B 测试。配置项本身要严格校验。代理对未知配置项的态度不一样Claude Code 可能报错Codex 可能只警告。你不能依赖代理帮你发现配置错误得在编排层自己校验一遍把明显的拼写错误、类型错误拦在前面。6.3 模型切换时的会话状态处理从云端模型切到本地模型或者从一个第三方接口切到另一个不能简单改配置重启。因为正在跑的会话里可能有未完成的任务直接重启会丢上下文。稳妥的做法是优雅切换先让当前会话把任务跑完或者保存进度再 kill 掉用新配置起一个新会话然后把必要的上下文迁移过去。这个迁移过程如果代理本身支持会话恢复比如 Claude Code 的 continue 功能就简单很多如果不支持你得手动把关键上下文喂给新会话。这里有个经验切换模型前先做一次完整的状态快照把当前会话的输出、任务进度、待办事项都记下来。切换出问题时你能快速回滚不至于两眼一抹黑。7. 我在实际使用中总结的几条经验先说一个反直觉的结论代理数量不是越多越好。我一开始觉得并行十个代理效率翻十倍实际跑下来发现超过四五个之后我的注意力全花在监控和切换上真正写代码的时间反而少了。代理并行的收益在任务之间真正独立时才成立如果任务之间有依赖并行只会带来协调成本。所以我现在一般同时跑两到三个一个主任务、一个辅助任务、一个后台长任务这个配置对我来说最舒服。第二个经验是日志一定要落盘。tmux 的缓冲区再大也是内存机器一重启就没了。代理跑出来的输出、状态流转记录、错误信息全部写到文件里按日期和会话名分目录。出问题时翻日志比重新复现快得多。我用的格式是每行一个 JSON包含时间戳、会话名、事件类型、内容摘要方便后续用脚本分析。第三个经验是别把编排逻辑写得太聪明。我见过有人给代理编排加了一堆自动决策什么自动重试、自动切换模型、自动调整参数结果系统复杂到自己都看不懂出问题根本没法排查。我的做法是编排层只做最笨的事起会话、抓输出、按固定规则应答、记录日志。所有需要判断的地方要么用最简单的规则要么推给人。系统越简单越可靠越容易维护。最后一个关于工具选型。Claude Code 和 Codex 各有各的好没必要非此即彼。我的用法是按任务类型分需要频繁交互、边聊边改的任务用 Claude Code边界清晰、可以闷头跑的任务用 Codex。openrig 这类编排工具的价值恰恰在于让你能同时用上两者的长处而不是被某一个工具绑死。至于具体怎么分配跑一段时间你自然就有感觉了这个没有标准答案适合自己的就是最好的。
阅读完成 · 觉得有帮助?