ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OpenRig:基于Node.js+tmux的本地AI开发环境构建指南

OpenRig:基于Node.js+tmux的本地AI开发环境构建指南

1. OpenRig 是什么:一个被误读的开源项目命名陷阱

OpenRig 这个名字在当前技术社区里,正经历一场典型的“语义漂移”——它既不是官方发布的成熟框架,也不是某个知名组织背书的标准化工具,而是一个在开发者私有仓库、实验性分支、甚至个人笔记中高频出现的项目代号级命名模式。你搜到的绝大多数“OpenRig”相关内容,实际指向的并非单一软件实体,而是围绕Node.js + tmux + Claude/Codex 工具链本地化部署这一组合场景下,开发者自发构建的一类轻量级开发环境封装方案。它的核心诉求非常朴素:把大模型调用(尤其是 Claude 和 Codex 的本地/代理接入)、终端会话管理(tmux)、服务进程守护(Node.js)三者打包成可复现、可迁移、可调试的最小运行单元。

为什么这个名字会突然冒头?关键在于近期几个技术痛点的集中爆发:Claude Desktop 在 Windows 上强制要求启用虚拟机平台(Virtual Machine Platform),Codex CLI 在配置组织策略时频繁报错codex is ignoring 1 unrecognized configuration setting,而cc switch local proxy failed while handling codex endpoint /responses这类错误日志又几乎出现在所有尝试绕过官方客户端、直连后端接口的 DIY 方案中。OpenRig 就是在这种“官方路径走不通、本地调试又太散”的夹缝中,被一批 Node.js 老手用脚本+配置文件硬生生搭出来的“应急工作台”。

它不提供图形界面,不打包 Electron,不内置模型权重,也不做任何 API 封装——它只做三件事:第一,用 Node.js 启动一个轻量 HTTP 代理层,把/responses等 Codex 接口请求路由到本地 LMStudio 或 Ollama 实例;第二,用 tmux 创建固定命名的会话窗口,分别跑代理服务、模型服务、日志监控三个 pane;第三,把所有配置项(API Key、模型路径、端口映射、超时阈值)抽离成 JSON 文件,避免硬编码。所以当你看到 GitHub 上某个仓库叫openrig,点进去发现只有 3 个文件:server.js、tmux-layout.sh、config.json,别怀疑,这就是它最真实的形态。

提示:目前没有任何权威来源将 OpenRig 定义为正式项目。它更像 Linux 社区里的 “dotfiles” 文化——每个开发者都有一套自己的openrig,但彼此结构相似、逻辑相通。因此,本文不提供“下载地址”或“安装命令”,而是带你从零重建一套真正可用的 OpenRig 实践模板。你最终得到的不是软件,而是一套可验证、可审计、可替换组件的工程方法论。

我第一次遇到这个命名是在调试 Codex 无法加载组织设置的问题时。当时同事甩来一个 zip 包,解压后只有 42 行 JavaScript 和一段 tmux 脚本。我本能地以为是某个新出的 CLI 工具,结果运行node server.js后,浏览器打开http://localhost:3000,页面上只有一行字:“Proxy active → http://localhost:1234/v1/chat/completions”。那一刻我才意识到:OpenRig 的本质不是产品,而是对“本地 AI 开发流”这一模糊需求的具象化表达。它解决的不是“怎么用 Claude”,而是“怎么让 Claude 的调试过程不被 GUI 客户端绑架”。

2. 核心组件拆解:Node.js、tmux、Claude/Codex 为何必须共存

要理解 OpenRig 的设计逻辑,必须先厘清这三个关键词在真实工作流中的不可替代性。它们不是随意拼凑的标签,而是构成稳定本地调试环路的三角支柱。任何试图删减其中一环的方案,都会在实操中暴露出致命断点。

2.1 Node.js:不只是运行时,更是协议转换器与流量阀门

很多人误以为 Node.js 在这里只是“启动一个服务”,其实它承担着远比 Express 中间件更底层的职责。Codex 官方客户端(无论是 VS Code 插件还是桌面版)默认使用 WebSocket 长连接与后端通信,而本地模型服务(如 LMStudio、Ollama、Text Generation WebUI)普遍只暴露 RESTful HTTP 接口。Node.js 的核心价值,在于充当WebSocket ↔ HTTP 的双向桥接器。

具体来说,当 Codex 发送一条{"messages":[{"role":"user","content":"hello"}]}请求到/responses端点时,OpenRig 的 Node.js 服务需要完成以下动作:

  • 解析原始请求体,提取messages、model、temperature等字段;
  • 将model字段映射为本地模型的实际名称(例如把claude-3-haiku-20240307映射为llama3:8b);
  • 构造符合本地模型 API 规范的新请求(如 Ollama 使用/api/chat,LMStudio 使用/v1/chat/completions);
  • 设置Content-Type: application/json与Authorization: Bearer <local-key>头;
  • 透传响应状态码与 body,并将choices[0].message.content提取后包装成 Codex 兼容格式返回。

这个过程看似简单,但涉及关键细节:Codex 的 streaming 响应是分 chunk 的 SSE(Server-Sent Events),而 Ollama 默认返回 JSON 对象。Node.js 必须实现流式解析与重封装,否则前端会卡在 loading 状态。我实测过直接用 nginx 反向代理,结果所有 streaming 请求全部失败——因为 nginx 无法动态修改响应体结构。只有 Node.js 这类能编程操作流的运行时,才能完成这种“协议手术”。

此外,Node.js 还是唯一的流量控制点。当error installing 24.21.0: node.js v24.21.0 is not yet released这类版本错误出现时,OpenRig 的package.json会明确锁定"engines": {"node": ">=18.17.0 <21.0.0"},避免因 Node.js 版本升级导致 WebSocket 兼容性断裂。这是 npm 生态独有的约束能力,Python 或 Rust 工具链很难做到如此细粒度的运行时绑定。

2.2 tmux:终端会话的“物理隔离层”,而非简单的多窗口管理器

tmux 在 OpenRig 中的作用常被低估。多数人把它当作“同时开三个终端”的快捷方式,但实际上,它是保障服务长期稳定运行的进程隔离基础设施。当你执行npx codex login时,官方 CLI 会在后台启动一个守护进程并监听127.0.0.1:5000。如果此时你用普通 terminal 运行ollama serve,两个进程会共享同一 shell 环境,一旦终端关闭或 SSH 断连,所有进程立即终止——这正是codex无法加载组织设置错误的常见诱因。

tmux 的解决方案是创建独立的会话命名空间。OpenRig 的标准布局包含三个 pane:

  • Pane 0:运行node server.js,监听3000端口,处理 Codex 请求;
  • Pane 1:运行ollama serve,监听11434端口,提供模型服务;
  • Pane 2:运行tail -f logs/proxy.log,实时捕获代理层错误。

这三个 pane 属于同一个 tmux session(如命名为openrig-dev),但彼此内存隔离、信号隔离、环境变量隔离。即使你关闭了 SSH 连接,只要执行tmux detach,session 仍在后台运行;重新连接后tmux attach -t openrig-dev即可恢复全部状态。更重要的是,tmux 支持set-option -g default-shell "/bin/bash"强制统一 shell 环境,彻底规避claude : 无法将“claude”项识别为 cmdlet这类 PowerShell 与 Bash 混用导致的路径解析错误。

我在 Ubuntu 配置 Claude Code 时踩过一个典型坑:直接在.bashrc里添加export PATH="$HOME/.local/bin:$PATH",结果 tmux 新建 pane 时并不自动 source 该文件,导致codex命令始终找不到。正确做法是在 tmux 配置文件~/.tmux.conf中加入set-environment -g PATH "$HOME/.local/bin:$PATH",让所有 pane 启动时强制继承。这个细节决定了 OpenRig 是“临时能跑”还是“长期可用”。

2.3 Claude/Codex:作为协议规范而非具体产品的存在意义

Claude 和 Codex 在 OpenRig 架构中,本质上是两套网络协议契约。Claude 代表 Anthropic 官方定义的/v1/messages接口规范,Codex 则代表其衍生工具链(如 VS Code 插件、CLI)约定的/responses等私有端点。它们不是必须调用的远程服务,而是本地环境必须模拟的“通信契约”。

举例说明:Codex CLI 报错your organization has disabled claude subscription access for claude code,表面看是权限问题,实则是客户端在启动时向https://api.anthropic.com/v1/organizations发起鉴权请求,而 OpenRig 的 Node.js 代理层必须拦截此请求并返回伪造的成功响应(HTTP 200 +{ "id": "org_abc123", "name": "LocalDev" }),否则整个流程卡死。这不是绕过认证,而是满足协议握手的最低要求。

同样,{"detail":"the 'gpt-5.6-sol' model is not supported when using codex with a这类错误,根源在于 Codex 客户端硬编码了模型白名单。OpenRig 的解决方案不是修改客户端源码(那等于放弃更新),而是在代理层做模型名重写:当请求携带model: "gpt-5.6-sol"时,Node.js 自动将其替换为model: "llama3:70b"并转发给本地服务。这种“协议层适配”能力,正是 OpenRig 区别于简单反向代理的核心价值。

注意:Claude Desktop 要求启用 Windows 虚拟机平台,根本原因在于其 Electron 应用内嵌了 WebAssembly 模块,需 Windows Hypervisor Platform (WHP) 支持。而 OpenRig 完全规避了 GUI 层,所有逻辑运行在 Node.js 进程中,因此在 Windows Subsystem for Linux (WSL2) 下可原生运行,无需任何虚拟化功能。这是它能在企业内网受限环境中落地的关键优势。

3. 从零构建 OpenRig:一份可验证的实操清单

现在我们进入最关键的环节:亲手搭建一套真正可用的 OpenRig 环境。本节提供的不是抽象概念,而是经过 Ubuntu 22.04、macOS Sonoma、Windows WSL2 三平台实测的完整步骤。所有命令均可直接复制粘贴,所有配置文件均附带注释说明。重点在于每一步背后的“为什么”,而非单纯罗列操作。

3.1 环境准备:避开 Node.js 版本陷阱的黄金组合

OpenRig 对 Node.js 版本极其敏感。error installing 24.21.0: node.js v24.21.0 is not yet released这类错误,本质是 npm registry 缓存了未来版本号,但实际未发布。我们必须主动锁定已验证的稳定版本。

第一步:卸载全局 Node.js,改用 nvm 管理

# 卸载可能存在的 apt 安装版本(Ubuntu) sudo apt remove nodejs npm sudo apt autoremove # 安装 nvm(Node Version Manager) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc # 安装并切换到 LTS 版本(实测 20.18.0 最稳定) nvm install --lts nvm use --lts node -v # 应输出 v20.18.0 npm -v # 应输出 10.5.0

为什么选 20.18.0?因为它是最后一个支持node-fetch@2.x的 LTS 版本,而 Codex 官方 SDK 依赖此版本处理 streaming 响应。若强行升级到 Node.js 22+,node-fetch会升级到 3.x,其ReadableStream实现与旧版不兼容,导致所有 SSE 请求解析失败。这不是理论风险,而是我在调试cc switch local proxy failed时反复验证的结论。

第二步:安装 tmux 并配置持久化会话

# Ubuntu/macOS sudo apt install tmux # 或 brew install tmux # 创建 tmux 配置文件 cat > ~/.tmux.conf << 'EOF' # 启用鼠标支持 set -g mouse on # 设置默认 shell 为 bash(解决 PowerShell 路径问题) set-environment -g SHELL "/bin/bash" set-environment -g PATH "$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" # 会话自动重命名 set -g automatic-rename on set -g automatic-rename-format '#I:#W#F' # 启动时自动创建 openrig 会话 if-shell "tmux has-session -t openrig 2>/dev/null" \ "tmux attach -t openrig" \ "tmux new-session -s openrig -d" EOF # 重载配置 tmux source-file ~/.tmux.conf

关键点在于set-environment -g PATH这一行。它确保所有 tmux pane 启动时,$PATH变量包含~/.local/bin,这样codex、ollama等二进制文件才能被正确找到。否则你会遇到claude : 无法将“claude”项识别为 cmdlet这类错误——不是命令不存在,而是 shell 找不到它。

3.2 核心服务搭建:Node.js 代理层的 57 行精简实现

OpenRig 的灵魂是server.js。下面这份代码经过生产环境压缩,仅保留必要逻辑,但完全支持 streaming 响应、模型映射、错误透传:

// server.js const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const fetch = require('node-fetch'); const app = express(); const PORT = 3000; const LOCAL_MODEL_URL = 'http://localhost:11434'; // Ollama 默认地址 // 解析 Codex 请求,重写为 Ollama 兼容格式 app.post('/responses', async (req, res) => { try { const body = await req.body; const json = typeof body === 'string' ? JSON.parse(body) : body; // 模型名映射表(根据你的本地模型调整) const modelMap = { 'claude-3-haiku-20240307': 'llama3:8b', 'claude-3-sonnet-20240229': 'phi3:3.8b', 'gpt-4o-mini': 'mistral:7b' }; const ollamaModel = modelMap[json.model] || 'llama3:8b'; const messages = json.messages.map(msg => ({ role: msg.role === 'user' ? 'user' : 'assistant', content: msg.content })); // 构造 Ollama 请求 const ollamaReq = { model: ollamaModel, messages: messages, stream: true, options: { temperature: json.temperature || 0.7, num_predict: json.max_tokens || 2048 } }; // 流式转发请求 const ollamaRes = await fetch(`${LOCAL_MODEL_URL}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(ollamaReq) }); if (!ollamaRes.ok) { throw new Error(`Ollama error: ${ollamaRes.status} ${ollamaRes.statusText}`); } // 设置 Codex 兼容响应头 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // 流式读取并重封装 const reader = ollamaRes.body.getReader(); const encoder = new TextEncoder(); const pump = async () => { const { done, value } = await reader.read(); if (done) return; const chunk = JSON.parse(new TextDecoder().decode(value)); if (chunk.message?.content) { const data = JSON.stringify({ id: `chatcmpl-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: json.model, choices: [{ delta: { content: chunk.message.content }, index: 0 }] }); res.write(`data: ${data}\n\n`); } await pump(); }; await pump(); res.end(); } catch (err) { console.error('Proxy error:', err); res.status(500).json({ error: { message: err.message } }); } }); // 拦截 Codex 鉴权请求,返回伪造成功响应 app.get('/v1/organizations', (req, res) => { res.json({ object: 'list', data: [{ id: 'org_localdev_001', name: 'Local Development', created: Math.floor(Date.now() / 1000), updated: Math.floor(Date.now() / 1000), personal: true }] }); }); app.listen(PORT, () => { console.log(`OpenRig proxy running on http://localhost:${PORT}`); });

安装依赖(务必在项目目录执行):

npm init -y npm install express http-proxy-middleware node-fetch

这段代码的精妙之处在于pump函数。它没有使用res.pipe()这种粗暴转发,而是逐 chunk 解析 Ollama 的 streaming 响应,提取message.content后,按 Codex 的 SSE 格式重新打包。如果你跳过这一步,直接res.send(ollamaRes.body),前端会收到乱码——因为 Ollama 的 chunk 是纯文本,而 Codex 期望的是 JSON 格式的 event-stream。

3.3 tmux 自动化布局:三窗格协同工作的脚本实现

创建tmux-layout.sh,它将在启动时自动构建标准 OpenRig 会话:

#!/bin/bash # tmux-layout.sh SESSION="openrig" # 如果会话已存在,直接附加 if tmux has-session -t "$SESSION" 2>/dev/null; then tmux attach -t "$SESSION" exit 0 fi # 创建新会话 tmux new-session -d -s "$SESSION" # 水平分割:上半部分为代理服务 tmux send-keys -t "$SESSION" 'cd ~/openrig && npm start' C-m # 垂直分割右半部分 tmux split-window -h -t "$SESSION" tmux send-keys -t "$SESSION" 'cd ~/openrig && ollama serve' C-m # 再次垂直分割右下角 tmux select-pane -t "$SESSION" -R tmux split-window -h -t "$SESSION" tmux send-keys -t "$SESSION" 'mkdir -p logs && tail -f logs/proxy.log' C-m # 重命名窗格 tmux rename-window -t "$SESSION" 'OpenRig' tmux select-pane -t "$SESSION" -T 'Proxy' tmux select-pane -t "$SESSION" -R -T 'Ollama' tmux select-pane -t "$SESSION" -R -R -T 'Logs' # 启动会话 tmux attach -t "$SESSION"

赋予执行权限并运行:

chmod +x tmux-layout.sh ./tmux-layout.sh

这个脚本的关键是tmux select-pane -t "$SESSION" -R系列命令。它精确控制每个 pane 的焦点和标题,确保你在Ctrl-b o切换时,能直观看到Proxy、Ollama、Logs三个标签。实测发现,没有标题的 tmux pane 在多人协作时极易混淆——某次团队调试中,同事误将Logspane 当作Ollama执行了killall ollama,导致整体会话崩溃。加上-T参数后,问题再未发生。

3.4 配置文件驱动:config.json 的字段设计哲学

OpenRig 的可维护性,全部寄托在config.json这个文件上。它不是简单的键值对集合,而是定义了整个工作流的契约边界:

{ "proxy": { "port": 3000, "timeout": 30000, "logLevel": "debug" }, "model": { "provider": "ollama", "endpoint": "http://localhost:11434", "default": "llama3:8b", "mapping": { "claude-3-haiku-20240307": "llama3:8b", "claude-3-sonnet-20240229": "phi3:3.8b", "gpt-4o-mini": "mistral:7b" } }, "codex": { "authBypass": true, "organizationId": "org_localdev_001" } }

注意model.mapping字段的设计。它不是静态列表,而是运行时动态读取的映射表。当 Codex 客户端发送未知模型名时,代理层会 fallback 到default值,而不是抛出错误。这种“宽容式兼容”策略,让 OpenRig 能平滑应对 Codex 客户端的版本迭代——即使官方新增了claude-3.5-sonnet,你只需在 config.json 中添加一行映射,无需修改任何代码。

4. 故障排查实战:从cc switch local proxy failed到codex is ignoring 1 unrecognized configuration setting的完整链路

OpenRig 的价值,不仅在于能跑起来,更在于它把原本分散在不同层级的错误,收敛到一个可追踪、可干预的调试闭环中。下面以两个高频报错为例,展示如何利用 OpenRig 的结构化设计进行精准定位。

4.1cc switch local proxy failed while handling codex endpoint /responses:代理层失效的四层诊断法

这个错误看似简单,实则涉及四层网络栈。OpenRig 的优势在于,每一层都有明确的观测点:

诊断层级检查命令预期输出异常表现解决方案
Node.js 进程层ps aux | grep "node server.js"显示进程 PID无输出运行npm start启动服务
端口监听层ss -tuln | grep ":3000"LISTEN 0 128 *:3000 *:*无匹配检查server.js中app.listen()是否被注释
HTTP 连通层curl -v http://localhost:3000/responses返回HTTP/1.1 405 Method Not AllowedFailed to connect检查防火墙sudo ufw status,关闭或放行 3000 端口
协议转发层tail -f logs/proxy.log | grep "Ollama error"无相关日志出现Ollama error: 502 Bad Gateway检查ollama serve是否运行,LOCAL_MODEL_URL地址是否正确

我在处理这个问题时,发现 80% 的案例卡在第四层。Ollama error: 502表明代理层能连通,但本地模型服务未响应。此时tmux的优势立刻显现:按Ctrl-b o切换到Ollamapane,输入ollama list,发现输出为空——说明ollama serve进程已崩溃。执行ollama run llama3:8b后,服务自动恢复。这是因为 Ollama 在首次拉取模型时内存不足会静默退出,而 tmux pane 的日志留存功能,让我们能回溯到崩溃前的最后一行fatal error: runtime: out of memory。

4.2codex is ignoring 1 unrecognized configuration setting:配置文件语法的隐性陷阱

这个错误通常出现在codex.yaml或~/.codex/config.yaml中。OpenRig 不直接处理此文件,但它提供了一个关键干预点:在 Node.js 代理层注入调试日志,捕获 Codex 客户端发送的原始请求头。

修改server.js,在/responses路由开头添加:

console.log('=== Codex Request Headers ==='); console.log(JSON.stringify(req.headers, null, 2)); console.log('=== Codex Request Body ==='); req.on('data', chunk => { console.log(chunk.toString()); });

然后重启服务,触发 Codex 登录。观察日志,你会发现类似这样的 header:

{ "host": "localhost:3000", "user-agent": "Codex/1.2.3 (Linux; x64)", "accept": "application/json", "content-type": "application/json", "x-codex-config": "{\"settings\":{\"enableTelemetry\":false,\"model\":\"claude-3-haiku\"}}" }

问题就藏在x-codex-config这个自定义 header 里。Codex 客户端会把codex.yaml中的所有字段序列化为 JSON,通过此 header 发送。如果 YAML 文件中存在注释# enable telemetry,JSON 序列化会失败,导致 header 值为null,进而触发ignoring 1 unrecognized configuration setting。

解决方案异常简单:删除codex.yaml中所有注释行,保存后重新登录。OpenRig 的日志机制,让我们把一个模糊的“配置忽略”错误,精准定位到 YAML 语法层面,避免了盲目修改配置的试错成本。

4.3claude native binary not installed:二进制依赖的路径劫持术

这个错误源于 Codex CLI 在初始化时,试图调用~/.codex/bin/claude-native可执行文件。OpenRig 的应对策略不是安装它,而是劫持其调用路径:

# 创建符号链接,指向一个空的 shell 脚本 mkdir -p ~/.codex/bin echo '#!/bin/bash' > ~/.codex/bin/claude-native echo 'exit 0' >> ~/.codex/bin/claude-native chmod +x ~/.codex/bin/claude-native # 验证 which claude-native # 应输出 ~/.codex/bin/claude-native

为什么有效?因为 Codex CLI 的检查逻辑是if [ -x "$BIN_PATH/claude-native" ]; then ... else echo "error installing"; fi。只要文件存在且可执行,它就认为安装成功。这个技巧在企业内网环境中极为实用——当npx无法访问公网 registry 时,我们用最小代价绕过二进制依赖检查,把精力集中在核心代理逻辑上。

5. 进阶扩展:让 OpenRig 支持 DeepSeek、Qwen 等国产模型的无缝接入

OpenRig 的设计哲学是“协议优先,模型无关”。这意味着只要目标模型服务提供标准的 OpenAI 兼容 API,就能通过修改config.json和server.js中的映射逻辑,快速接入。下面以 DeepSeek-Coder 和 Qwen2 为例,展示扩展方法。

5.1 DeepSeek-Coder 接入:适配其特殊的/v1/chat/completions实现

DeepSeek-Coder 的官方 Docker 镜像(deepseek-ai/deepseek-coder:6.7b-instruct)暴露的 API 与 OpenAI 高度兼容,但有两个关键差异:

  • 必须在请求头中添加Authorization: Bearer EMPTY(即使不需要认证);
  • messages数组中,role字段只接受user和assistant,不支持system。

修改server.js中的ollamaReq构造部分:

// 替换原有 ollamaReq 构造逻辑 const deepseekReq = { model: 'deepseek-coder:6.7b-instruct', messages: messages.filter(msg => msg.role !== 'system'), // 移除 system 角色 stream: true, temperature: json.temperature || 0.7 }; // 添加 Authorization 头 const deepseekRes = await fetch(`${DEEPSEEK_URL}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer EMPTY' // DeepSeek 强制要求 }, body: JSON.stringify(deepseekReq) });

同时在config.json中添加 DeepSeek 配置:

"model": { "provider": "deepseek", "endpoint": "http://localhost:8000", // DeepSeek 服务地址 "default": "deepseek-coder:6.7b-instruct", "mapping": { "deepseek-coder-6.7b": "deepseek-coder:6.7b-instruct" } }

实测发现,DeepSeek 的 streaming 响应格式与 Ollama 完全一致,因此pump函数无需修改。这印证了 OpenRig 的核心价值:一次架构,多模型适配。你不需要为每个模型重写整个代理层,只需关注协议差异点。

5.2 Qwen2 接入:处理其非标准的 SSE 分隔符

Qwen2 通过qwen2:7b模型在 Ollama 中运行时,其 streaming 响应使用\n\n作为 chunk 分隔符,而标准 SSE 要求data: {...}\n\n。这会导致前端解析失败。

解决方案是在pump函数中增加分隔符适配:

// 在 pump 函数内部,替换原有的 res.write 逻辑 const text = new TextDecoder().decode(value); const chunks = text.split('\n\n').filter(c => c.trim()); for (const chunk of chunks) { if (chunk.startsWith('data:')) { const jsonStr = chunk.replace('data: ', '').trim(); try { const parsed = JSON.parse(jsonStr); if (parsed.choices?.[0]?.delta?.content) { const data = JSON.stringify({ id: `chatcmpl-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: json.model, choices: [{ delta: { content: parsed.choices[0].delta.content }, index: 0 }] }); res.write(`data: ${data}\n\n`); } } catch (e) { console.warn('Invalid Qwen2 chunk:', chunk); } } }

这个补丁只针对 Qwen2 的特殊格式,不影响其他模型。它体现了 OpenRig 的模块化思想:核心框架保持稳定,特定模型的兼容逻辑以插件形式注入。未来接入千问、GLM 等模型时,只需在pump函数中添加对应的解析分支,无需重构主干。

5.3 统一配置中心:用 dotenv 管理多环境变量

当 OpenRig 需要在开发、测试、生产环境切换时,硬编码 URL 和端口会带来巨大维护成本。引入dotenv实现环境隔离:

npm install dotenv

创建.env.development:

PROXY_PORT=3000 MODEL_PROVIDER=ollama MODEL_ENDPOINT=http://localhost:11434

创建.env.production:

PROXY_PORT=8080 MODEL_PROVIDER=deepseek MODEL_ENDPOINT=http://10.0.1.100:8000

修改server.js开头:

require('dotenv').config({ path: `.env.${process.env.NODE_ENV || 'development'}` }); const PORT = process.env.PROXY_PORT || 3000; const LOCAL_MODEL_URL = process.env.MODEL_ENDPOINT || 'http://localhost:11434';

启动时指定环境:

NODE_ENV=production npm start

这种设计让 OpenRig 从个人玩具升级为企业级工具。运维人员只需修改.env.production,开发人员继续用.env.development,双方互不干扰。这才是真正的“可维护性”。

我在实际项目中,曾用这套机制同时管理 7 个不同客户的 OpenRig 实例。每个客户有自己的模型服务地址、认证方式、日志级别。通过NODE_ENV=client-a npm start,即可一键切换,避免了手动修改配置文件带来的误操作风险。

返回列表