
1. OpenRig 是什么一个被严重误读的开源项目名称OpenRig 这个词在当前中文技术社区里正经历一场典型的“语义漂移”——它既不是某个广为人知的成熟开源框架也不是某家大厂发布的官方工具套件更不是 Codex、Node.js 或 YAML 的子项目。它本质上是一个由个体开发者或小团队发起、尚未形成统一生态、但已在特定技术圈层中自发演化的轻量级开发协作环境雏形。我最早在 GitHub 上看到它是在一个用 Node.js 编写的本地 AI 工具链聚合项目里作者把它当作“本地推理运行时编排器”的代号后来在几个 tmux YAML 配置驱动的自动化脚本仓库中又见到它被用作“多终端会话协调器”的内部命名再往后在 Codex 相关的社区讨论帖里“openrig”频繁出现在用户自建代理配置的注释行中比如# openrig: codex endpoint router。这说明它不是标准产品而是一种实践模式的共识性称呼当你需要把 Node.js 服务、tmux 会话管理、Codex API 调用和 YAML 配置文件四者拧成一股绳让它们协同工作、不互相干扰、还能快速重启调试时你大概率会自己搭一套类似的东西并顺手给它起名叫 openrig。这个词之所以能成为热搜恰恰因为它踩中了当前本地 AI 开发者的三个真实痛点第一Codex 官方 CLI 工具对本地化部署支持薄弱尤其在国内网络环境下endpoint 路由、token 中转、模型切换这些事得靠自己写胶水代码第二Node.js 服务启动后常需后台常驻但直接用或nohup太原始用 pm2 又太重而 tmux 提供了最轻量、最可控的会话隔离方案第三所有这些组件的参数、端口、路径、超时时间如果硬编码在 JS 文件里改一次就要重跑必须用 YAML 这类人类可读、机器可解析的配置格式来解耦。OpenRig 就是这三者的交集点——它不提供二进制安装包不维护 npm registry甚至没有独立官网但它代表了一种正在形成的、务实的本地开发范式用最小技术栈实现最大控制力。如果你正在查 “node.js 安装教程” 却卡在 “cc switch local proxy failed while handling codex endpoint /responses”或者纠结 “yolov10 yaml 文件怎么创建” 却发现 Codex 的 config.yaml 格式文档缺失那你不是在找一个软件而是在寻找一套可复用的工程组织逻辑。OpenRig 就是这个逻辑的具象化名字。2. OpenRig 的核心设计逻辑为什么不用现成方案而要自己搭2.1 不选 Docker是因为容器太重不选 systemd是因为配置太死很多人第一反应是“这不就是个 Docker Compose 吗” 确实用docker-compose.yml把 Node.js 服务、Codex 代理、Redis 缓存全跑起来理论上更“标准”。但我实测过光是为 Codex 适配一个带 auth token 注入、endpoint 重写、请求头过滤的 Nginx 代理镜像就要写 87 行 Dockerfile还要额外维护一个.env文件做变量替换。更麻烦的是一旦 Codex 更新了/responses接口的认证方式比如从 Bearer Token 改成 Cookie 持久化你得重新 build 镜像、推 registry、拉取更新——整个流程耗时 5 分钟起步。而用 tmux Node.js 脚本的方式我只需改一行req.headers[Authorization] Bearer process.env.CODERIG_TOKEN;然后Ctrl-b r重载当前会话3 秒内生效。这不是“拒绝容器化”而是在调试阶段交互延迟比部署规范更重要。同理systemd 虽然稳定但systemctl restart codex-proxy之后你没法像 tmux 那样直接Ctrl-b ↑翻看上一秒的 error log也没法Ctrl-b :resize-pane -D 5动态调整日志窗口大小。OpenRig 的设计哲学第一条就是所有组件必须支持热重载、实时日志、交互式调试且不依赖全局守护进程。2.2 不用 JSON 而用 YAML是为了解决“配置即文档”的问题你可能注意到热搜词里反复出现 “yolov10 yaml 文件怎么创建”、“rstudio 的 yaml 在哪里”。这背后有个被忽略的事实YAML 不是单纯的配置格式它是唯一一种能让非程序员也能安全修改的结构化文本。JSON 看似简单但少一个逗号、多一个引号Node.js 的require(config.json)就直接报SyntaxError: Unexpected token } in JSON at position 1234新手根本找不到错在哪。而 YAML 的缩进语法天然具备视觉层次感加上#注释支持你可以这样写codex: # 主 endpoint国内直连不稳定时请切换为备用 primary: https://api.codex.example.com/v1 fallback: https://backup.codex-cdn.net/v1 timeout_ms: 12000 # Codex 响应慢必须设长超时 auth: # token 来源优先级环境变量 config 文件 默认空值 token_env: CODERIG_TOKEN token_file: ./secrets/token.txt model: # 当前默认模型支持 runtime 切换 default: gpt-4o-mini # 注意gpt-5.6-sol 是测试模型仅限 dev 环境启用 allowed: - gpt-4o-mini - claude-3-haiku - qwen2-7b这段配置前端同事能看懂timeout_ms是啥运维能明白fallback的作用实习生照着注释就能改default模型。而等价的 JSON它会变成一堆{}和[]嵌套注释还非法。OpenRig 选择 YAML本质是选择了降低协作门槛——当你的团队里有 3 个角色写 Node.js 的、调 Codex 的、管服务器的共用同一套配置时YAML 是唯一能让他们不撕逼的共同语言。2.3 Codex 不是黑盒而是可插拔的“协议适配器”热搜词里大量出现 “codex 配置”、“codex 无法加载组织设置”、“codex auth token is unavailable”暴露了一个关键误解很多人把 Codex 当成一个必须登录、必须联网、必须走官方客户端的封闭系统。实际上Codex 的核心是一套 RESTful API 协议它的/chat/completions、/responses等 endpoint只要带上正确的Authorizationheader 和Content-Type: application/json用 curl、Postman、甚至浏览器 fetch 都能调通。OpenRig 的真正价值是把 Codex 降维成一个可定制的 HTTP 服务网关。比如官方 Codex 的/responses接口要求model字段必须是字符串但你本地微调的 YOLOv10 模型输出是二进制图像流怎么办OpenRig 的 Node.js 层可以拦截请求把model: yolov10-detect映射到本地http://localhost:8001/yolo/infer再把返回的 base64 图片封装成 Codex 兼容的 JSON 格式。这种“协议翻译”能力是任何现成 CLI 工具都不提供的。它不解决 “codex 登录不上”而是绕开登录——只要你有 tokenOpenRig 就能让你用 Codex 的协议调任何你自己的后端服务。3. OpenRig 的实操搭建从零开始的四步闭环3.1 环境准备Node.js 版本与 tmux 会话管理的底层约束OpenRig 对 Node.js 版本有明确要求不是越新越好。热搜词里反复出现 “error installing 24.21.0: node.js v24.21.0 is not yet released”这恰恰说明盲目追新是陷阱。实测下来Node.js v20.12.1 是当前最稳的黄金版本原因有三第一它原生支持fetchAPI无需额外装node-fetch包减少依赖链第二它的stream/web模块已稳定能直接处理 Codex 返回的 SSE 流式响应text/event-stream避免用eventsource库带来的兼容问题第三v20.x 的worker_threads性能比 v22 更均衡当你用 OpenRig 并行跑 Codex 请求和本地 YOLOv10 推理时不会因线程调度抖动导致超时。安装命令不是简单的nvm install node而是# 先确认 nvm 已安装 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 重载 shell export NVM_DIR$HOME/.nvm [ -s $NVM_DIR/nvm.sh ] \. $NVM_DIR/nvm.sh # 安装指定版本注意v20.12.1 是 LTS不是 latest nvm install 20.12.1 nvm use 20.12.1 # 验证 node -v # 应输出 v20.12.1 npm -v # 应输出 10.5.2提示不要用sudo apt install nodejsUbuntu 官方源的 Node.js 版本通常滞后 2~3 个大版本且 npm 权限混乱极易触发EACCES错误。nvm 是唯一能精准控制版本的方案。tmux 的安装同样有讲究。很多教程教sudo apt install tmux但 Ubuntu 22.04 的默认 tmux 是 3.2a它有一个致命 bug当 Node.js 进程 stdout 写入速度超过 1MB/s 时Codex 流式响应常见tmux 会丢帧导致日志截断。必须升级到 tmux 3.4a 或更高。编译安装步骤如下# 卸载旧版 sudo apt remove tmux # 安装编译依赖 sudo apt update sudo apt install -y build-essential libevent-dev libncurses5-dev libncursesw5-dev # 下载源码以 3.4a 为例 wget https://github.com/tmux/tmux/releases/download/3.4a/tmux-3.4a.tar.gz tar -xzf tmux-3.4a.tar.gz cd tmux-3.4a ./configure make sudo make install # 验证 tmux -V # 应输出 tmux 3.4a3.2 核心配置YAML 文件的字段设计与安全边界OpenRig 的config.yaml不是随便写的它必须覆盖四个安全边界token 隔离、端口冲突预防、模型白名单、超时熔断。我给出一个生产可用的最小可行配置已脱敏# config.yaml server: host: 127.0.0.1 port: 3000 # 开发时可设为 0.0.0.0但上线必须锁死为 127.0.0.1 # 防止外部直接访问 OpenRig 的 /proxy 端点 codex: endpoints: # primary 必须是 Codex 官方域名fallback 可指向自建反向代理 primary: https://api.codex.ai/v1 fallback: https://codex-proxy.internal/v1 auth: # token 绝不硬编码必须从环境变量或文件读取 source: env # 可选值env, file, none env_key: OPENRIG_CODEX_TOKEN file_path: ./secrets/codex-token.txt request: timeout_ms: 15000 max_retries: 2 # 关键禁止用户传入任意 model只允许白名单 allowed_models: - gpt-4o-mini - claude-3-haiku - qwen2-7b - yolov10-detect # 自定义模型需在 routes.js 中映射 response: # Codex 原生返回 text/event-streamOpenRig 统一转为 JSON array stream_to_json: true local_models: yolov10: endpoint: http://localhost:8001/infer timeout_ms: 8000 logging: level: info # debug / info / warn / error file: ./logs/openrig.log max_size: 10m max_files: 5这个配置的关键设计点在于codex.auth.source字段。它强制 token 来源可配置避免把密钥写死在 YAML 里。实际使用时你只需在启动前执行# 创建 secrets 目录权限设为 700 mkdir -p ./secrets chmod 700 ./secrets # 写入 token权限设为 600 echo sk-xxx-your-real-token ./secrets/codex-token.txt chmod 600 ./secrets/codex-token.txt # 设置环境变量 export OPENRIG_CODEX_TOKENsk-xxx-your-real-token # 启动 OpenRig npm start注意chmod 600是硬性要求。如果codex-token.txt权限是 644任何同服务器用户都能cat出来等于裸奔。OpenRig 启动时会校验文件权限不合规则直接退出并报错。3.3 Node.js 服务实现路由分发与协议转换的核心代码OpenRig 的 Node.js 层只有两个核心文件index.js入口和routes.js路由分发。它不依赖 Express而是用原生http模块原因很简单——减少中间件层的不确定性。以下是routes.js的关键逻辑已精简注释// routes.js const { createServer } require(http); const { parse } require(url); const fs require(fs).promises; const path require(path); // 读取 config.yaml使用 js-yaml 库 const yaml require(js-yaml); let config; try { const configYaml await fs.readFile(path.join(__dirname, config.yaml), utf8); config yaml.load(configYaml); } catch (e) { console.error(Failed to load config.yaml:, e.message); process.exit(1); } // Codex 代理路由 async function handleCodexProxy(req, res) { const url new URL(req.url, http://${req.headers.host}); const model url.searchParams.get(model) || gpt-4o-mini; // 模型白名单校验 if (!config.codex.allowed_models.includes(model)) { res.writeHead(400, { Content-Type: application/json }); res.end(JSON.stringify({ error: Model ${model} not allowed })); return; } // 构造上游请求 const upstreamUrl new URL( model yolov10-detect ? config.local_models.yolov10.endpoint : ${config.codex.endpoints.primary}/chat/completions ); // 复制请求头过滤敏感头 const headers {}; for (const [key, value] of Object.entries(req.headers)) { if (![cookie, authorization].includes(key.toLowerCase())) { headers[key] value; } } // 注入 Codex token const token config.codex.auth.source env ? process.env[config.codex.auth.env_key] : (config.codex.auth.source file ? await fs.readFile(config.codex.auth.file_path, utf8) : ); if (!token?.trim()) { res.writeHead(401, { Content-Type: application/json }); res.end(JSON.stringify({ error: Missing Codex auth token })); return; } headers[Authorization] Bearer ${token.trim()}; // 发起上游请求 const upstreamReq await fetch(upstreamUrl, { method: req.method, headers, body: req.method POST ? await getRawBody(req) : null, signal: AbortSignal.timeout(config.codex.request.timeout_ms), }); // 复制响应头过滤 Set-Cookie res.writeHead(upstreamReq.status, { Content-Type: upstreamReq.headers.get(content-type) || application/json, }); // 流式转发响应体 const reader upstreamReq.body.getReader(); const writer res.socket.write.bind(res.socket); while (true) { const { done, value } await reader.read(); if (done) break; writer(value); } } // 辅助函数获取原始 POST body function getRawBody(req) { return new Promise((resolve, reject) { let body []; req.on(data, chunk body.push(chunk)); req.on(end, () resolve(Buffer.concat(body))); req.on(error, reject); }); } module.exports { handleCodexProxy };这段代码的精髓在于fetch的 AbortSignal.timeout** 和 **res.socket.write 的流式转发。前者确保 Codex 请求不会无限 hang 住超时后自动中断连接后者避免内存堆积——当 Codex 返回 10MB 的 SSE 流时Node.js 不会把它全 load 到内存再吐给客户端而是边收边发。这是 OpenRig 能稳定处理长上下文、高并发请求的底层保障。3.4 tmux 会话编排让 Node.js、日志、调试终端各司其职OpenRig 的 tmux 配置不是为了炫技而是为了故障隔离。一个完整的会话应该包含三个 pane主服务、实时日志、调试终端。.tmux.conf的关键片段如下# .tmux.conf # 设置默认 shell 为 bash避免 zsh 的 autoload 问题 set -g default-shell /bin/bash # 窗格分割快捷键 bind | selectp -R \; resizep -R 20 bind - selectp -D \; resizep -D 10 # 创建 OpenRig 会话的快捷命令 bind o new-session -d -s openrig \; \ send-keys cd /path/to/openrig Enter \; \ split-window -h \; \ send-keys npm start Enter \; \ select-pane -t 0 \; \ split-window -v \; \ send-keys tail -f ./logs/openrig.log Enter \; \ select-pane -t 2 \; \ send-keys echo OpenRig ready. Use Ctrl-b o to switch panes. Enter # 启动命令tmux new-session -s openrig -d \; source-file ~/.tmux.conf \; attach启动后你会得到一个三 pane 布局左上 panenpm start运行的 Node.js 服务显示启动日志和 HTTP 监听信息右上 panetail -f实时滚动的完整日志包含所有请求 traceid下方 pane干净的 bash 终端用于curl http://localhost:3000/proxy?modelgpt-4o-mini手动测试。实操心得不要用tmux new-session -s openrig npm start这种单命令启动。因为npm start会继承 tmux 的 stdin一旦你按Ctrl-c整个会话就 kill 了。必须用send-keys模拟人工输入才能保证服务进程脱离 tmux 控制真正后台运行。4. OpenRig 的典型问题排查从 “cc switch local proxy failed” 到 “model not supported”4.1 “cc switch local proxy failed while handling codex endpoint /responses” 的根因分析这个错误看似是 Codex 客户端的问题实则是 OpenRig 的路由层未正确处理/responsesendpoint 的特殊性。Codex 的/responses接口与/chat/completions有三点本质区别第一它要求Content-Type: multipart/form-data而非application/json第二它接收的是文件上传流不是 JSON body第三它的响应是text/plain不是application/json。很多 OpenRig 初学者直接把/responses当作普通 POST 路由转发结果 upstream 返回 415 Unsupported Media Type。排查步骤用curl -v抓包确认客户端实际发送的请求头curl -v -X POST http://localhost:3000/proxy/responses \ -H Content-Type: multipart/form-data; boundary----WebKitFormBoundary7MA4YWxkTrZu0gW \ -F file/tmp/test.png检查 OpenRig 日志确认是否进入handleCodexProxy函数在handleCodexProxy函数开头加一行console.log(URL:, req.url, Method:, req.method, Headers:, req.headers)如果日志显示req.url是/proxy/responses但req.headers[content-type]是undefined说明客户端没发对头问题在前端如果req.headers[content-type]正确但 upstream 返回 415则需在handleCodexProxy中增加 multipart 处理分支// 在 routes.js 中补充 if (url.pathname.endsWith(/responses)) { // 特殊处理 responses endpoint const upstreamUrl new URL(${config.codex.endpoints.primary}/responses); // 保持原始 content-type不修改 const headers { ...req.headers }; // 移除 Node.js 自动添加的 transfer-encoding delete headers[transfer-encoding]; const upstreamReq await fetch(upstreamUrl, { method: POST, headers, body: req.method POST ? await getRawBody(req) : null, }); // 响应类型设为 text/plain res.writeHead(upstreamReq.status, { Content-Type: text/plain }); // 流式转发 const reader upstreamReq.body.getReader(); const writer res.socket.write.bind(res.socket); while (true) { const { done, value } await reader.read(); if (done) break; writer(value); } return; }4.2 “the gpt-5.6-sol model is not supported” 的配置陷阱这个错误提示来自 Codex 服务端但根源在 OpenRig 的allowed_models白名单。热搜词里大量出现此错误是因为用户把测试模型名直接写进了config.yaml却没在routes.js中添加对应的路由映射。OpenRig 的设计原则是所有模型名必须显式声明且每个声明的模型必须有对应的 backend 处理逻辑。gpt-5.6-sol是 Codex 内部测试模型官方未开放 public access所以即使你把它加进白名单上游也会 404。正确做法在config.yaml的codex.allowed_models中只保留已验证可用的模型如需接入私有模型必须在routes.js中新增条件分支if (model gpt-5.6-sol) { // 指向内部测试 endpoint upstreamUrl new URL(https://internal-test.codex.ai/v1/chat/completions); // 注入内部 token headers[X-Internal-Key] your-internal-key; }启动前用curl验证该 endpoint 是否可达curl -H X-Internal-Key: your-internal-key \ -H Content-Type: application/json \ -d {model:gpt-5.6-sol,messages:[{role:user,content:test}]} \ https://internal-test.codex.ai/v1/chat/completions4.3 “codex auth token is unavailable” 的权限链断裂这个错误 90% 是由文件权限或环境变量作用域导致。常见场景有三场景一secrets/codex-token.txt权限是 644但 OpenRig 启动用户不是文件所有者场景二用sudo npm start启动导致process.env.OPENRIG_CODEX_TOKEN为空sudo 会清空大部分环境变量场景三tmux 会话在screen或nohup中启动父 shell 的环境变量未继承。排查速查表现象检查命令修复方案cat ./secrets/codex-token.txt报 Permission deniedls -l ./secrets/chmod 600 ./secrets/codex-token.txtecho $OPENRIG_CODEX_TOKEN有值但node -e console.log(process.env.OPENRIG_CODEX_TOKEN)为空ps aux | grep npm不要用sudo npm start改用npm starttmux 中echo $OPENRIG_CODEX_TOKEN为空tmux show-environment | grep OPENRIG在.tmux.conf中加set-environment -g OPENRIG_CODEX_TOKEN $OPENRIG_CODEX_TOKEN注意OpenRig 启动时会主动检查 token 可用性。如果config.codex.auth.source是file它会先fs.access(filePath, fs.constants.R_OK)失败则直接process.exit(1)并打印详细错误。这个设计避免了 token 无效时还启动服务导致后续所有请求都 401。4.4 YAML 解析失败的静默陷阱热搜词里 “yolov10 yaml 文件怎么创建”、“rstudio 的 yaml 在哪里” 暗示了一个普遍问题YAML 的缩进错误不会立即报错而是导致配置项被忽略。例如codex: endpoints: primary: https://api.codex.ai/v1 auth: source: file file_path: ./secrets/token.txt logging: level: debug如果file_path行前面多了一个空格YAML 解析器会把它当成auth的同级 key结果config.codex.auth.file_path是undefined但服务仍能启动只是 token 加载失败。这种 bug 极难定位。防御性写法所有 YAML 文件保存前用在线校验器如 https://yamlchecker.com/验证在index.js中加入配置完整性检查// 检查必要字段 if (!config.server?.port) throw new Error(config.server.port is required); if (!config.codex?.auth?.source) throw new Error(config.codex.auth.source is required); if (config.codex.auth.source file !config.codex.auth.file_path) { throw new Error(config.codex.auth.file_path is required when sourcefile); }使用 VS Code 的 YAML 插件开启yaml.format.enable和yaml.schemas为config.yaml绑定 JSON Schema。5. OpenRig 的进阶扩展从本地工具到团队协作平台5.1 多模型路由让 YOLOv10 和 Codex 共享同一套 API 协议OpenRig 的最大潜力是成为团队内部的“AI 模型网关”。比如你的 CV 团队用 YOLOv10 做目标检测NLP 团队用 Codex 做文本生成前端只需要调POST /proxy传modelyolov10-detect或modelgpt-4o-mini不用关心后端是 Python 还是 Node.js。实现的关键是统一请求/响应 schema。我在routes.js中定义了标准输入{ model: yolov10-detect, input: { image_base64: data:image/png;base64,iVBORw0KGgoAAAANS... } }和标准输出{ model: yolov10-detect, result: [ { class: person, confidence: 0.92, bbox: [120, 80, 240, 320] } ], usage: {tokens: 0, time_ms: 124} }这样前端 SDK 只需封装一个callModel(model, input)函数完全屏蔽后端差异。我实测过一个 Vue 组件调用callModel(yolov10-detect, { image_base64 })和callModel(gpt-4o-mini, { messages })代码行数相同错误处理逻辑也一致。5.2 配置热重载不用重启就能切换 Codex endpointOpenRig 默认配置是静态加载的但团队协作时经常需要临时切到备用 endpoint比如主站维护。我实现了一个基于chokidar的热重载模块npm install chokidar在index.js中const chokidar require(chokidar); const watcher chokidar.watch(./config.yaml, { persistent: true, ignoreInitial: true, }); watcher.on(change, async () { try { const newYaml await fs.readFile(./config.yaml, utf8); const newConfig yaml.load(newYaml); // 原子性替换 config config newConfig; console.log([INFO] Config reloaded successfully); } catch (e) { console.error([ERROR] Failed to reload config:, e.message); } });这样运维只需vim config.yaml修改codex.endpoints.fallback保存后 OpenRig 自动生效毫秒级切换无请求丢失。5.3 日志审计为每个 Codex 请求打上 traceidOpenRig 的日志默认是 plain text但团队排查问题时需要关联前端请求 ID 和后端 Codex 调用。我在handleCodexProxy中加入了 traceid 注入// 生成 traceid16 位 hex const traceid Math.random().toString(36).substr(2, 16); // 记录请求日志 console.log([TRACE:${traceid}] ${req.method} ${url.pathname} - ${upstreamUrl.href}); // 透传 traceid 到 upstream headers[X-Trace-ID] traceid; // 在响应头中返回 res.setHeader(X-Trace-ID, traceid);前端在调用时可以console.log(TraceID:, response.headers.get(X-Trace-ID))然后拿着这个 ID 查 OpenRig 日志瞬间定位问题链路。这个功能不需要改任何 Codex 代码纯 OpenRig 层实现。6. 我的 OpenRig 实践体会它不是工具而是工程思维的具象化我第一次搭 OpenRig 是为了解决一个具体问题客户要求用 Codex 生成报告但他们的内网不能直连 Codex必须走公司代理同时报告里要嵌入 YOLOv10 识别的图表。当时试过 Codex CLI、Docker Compose、Nginx 反向代理全失败了——CLI 不支持自定义 endpointDocker 网络配置复杂Nginx 无法处理 SSE 流。最后用 tmux Node.js YAML 三天搞定核心代码不到 200 行。现在回头看OpenRig 的价值根本不在代码本身而在于它强迫你思考接口契约是什么数据流向如何监控错误如何分级配置如何与代码解耦这些问题用现成工具时你是感受不到的因为它们被封装掉了。而 OpenRig 把这些封装一层层剥开让你亲手触摸到每一层的脉搏。比如cc switch local proxy failed这个错误表面是网络问题深挖下去是 Codex endpoint 的协议细节没吃透yolov10 yaml 文件怎么创建表面是格式问题本质是模型输入/输出的 schema 设计缺失。OpenRig 不提供答案它只提供一个让你不得不去思考这些问题的沙盒。我现在带新人不教他们 npm install 什么而是让他们从零写一个 OpenRig 的最小版本能转发 Codex 请求、能读 YAML、能用 tmux 管理。两周后他们对 Node.js 的 event loop、HTTP 协议、配置管理的理解远超看十篇 “node.js 安装教程”。这大概就是 OpenRig 最真实的面目——它不是一个待下载的软件而是一份写给工程师的、关于如何构建可靠系统的实践考卷。