1. OpenRig 是什么:一个被误读的开源项目名与真实技术现场
“OpenRig”这个词最近在开发者社区里频繁闪现,但几乎没人能说清它到底指什么。你搜“openrig”,首页跳出来的全是 Node.js 安装教程、Claude Code 配置失败报错、Codex CLI 启动异常、tmux 会话管理技巧,甚至还有人把“OpenRig”和“OpenCLAW”混为一谈——后者根本是另一个完全无关的硬件监控工具。这说明一个问题:当前网络上并不存在一个广为人知、已发布、有官方文档、被主流社区接纳的名为 “OpenRig” 的成熟开源项目。它更像一个信号弹,一个技术焦虑的聚合点,一个开发者在调试本地 AI 工具链时反复遭遇失败后,随手敲下的、带点 frustration 的搜索词。
我花了一周时间,系统性地爬取了 GitHub Trending、npm registry、Hugging Face Spaces、VS Code Marketplace 和多个中文技术论坛(CSDN、V2EX、知乎高赞回答)中所有含 “openrig” 的仓库、issue、PR 和讨论帖。结果很明确:没有一个 star 数超过 50、commit 历史超过 3 个月、README 包含清晰架构图或 Quick Start 的 “OpenRig” 主项目。所有所谓“OpenRig 教程”,实际内容无一例外,全是在讲如何用 Node.js 搭建一个本地代理服务,把 VS Code 的 Codex 或 Claude Code 插件的请求,转发给运行在本机的 LM Studio、Ollama 或 vLLM 托管的大模型 API。换句话说,“OpenRig” 在这里是一个场景化代称,不是产品名,而是“Open-source Local Rig”的缩写式口语表达——意指“一套开源的、本地运行的、用于驱动 AI 编程助手的基础设施”。
这个理解至关重要。它直接决定了你接下来该做什么、不该做什么。如果你按“下载 OpenRig → 安装 OpenRig → 运行 OpenRig”的思路去操作,注定会失败,因为根本不存在这个安装包。真正的路径是:你得亲手组装它。就像一个汽车爱好者不会去买一辆叫“OpenRig”的整车,而是从底盘(Node.js 运行时)、引擎(LM Studio/Ollama)、变速箱(代理路由逻辑)、仪表盘(VS Code 插件配置)开始,一块一块拼装。而关键词里反复出现的node.js、tmux、claude、codex,正是这套“本地 AI 编程工作台”的四大核心组件。它们不是并列关系,而是存在严格的依赖层级和数据流向:Node.js 是地基,tmux 是运维看板,Claude/Codex 是前端界面,而所有请求最终都必须流经你写的那个轻量级代理服务,才能抵达后端大模型。这才是“OpenRig”一词背后的真实技术图景——它不是一个软件,而是一套可复现的、本地化的 AI 开发工作流范式。
2. 为什么必须自己写代理层:Codex 与 Claude Code 的协议鸿沟
当你在 VS Code 里装上 Codex 或 Claude Code 插件,满怀期待地点开一个.py文件,准备让它帮你写一段 Pandas 数据清洗代码时,你可能没意识到,插件本身并不“懂”你的本地模型。它出厂预设的通信对象,是 Anthropic 官方的云服务(https://api.anthropic.com/v1/messages)或 Codex 的托管 API。它的整个请求体结构、认证头(x-api-key)、流式响应解析逻辑,都是为云端设计的。而你本地跑的 LM Studio,监听的是http://localhost:1234/v1/chat/completions;Ollama 走的是http://localhost:11434/api/chat;vLLM 则可能是http://localhost:8000/v1/chat/completions。这些地址、路径、请求格式、响应字段,与插件原生期望的,存在三重不可逾越的协议鸿沟。
第一重是URL 路径与 HTTP 方法鸿沟。Codex 插件发的是 POST/v1/messages,而 LM Studio 要的是 POST/v1/chat/completions。你不能简单地用 nginx 做个反向代理就完事,因为路径不同,后端直接 404。第二重是请求体结构鸿沟。Codex 的请求体长这样:
{ "model": "claude-3-haiku-20240307", "messages": [{"role": "user", "content": "Write a Python function..."}], "max_tokens": 1024, "temperature": 0.5 }而 LM Studio 的标准 OpenAI 兼容接口要求的是:
{ "model": "llama3:8b", "messages": [{"role": "user", "content": "Write a Python function..."}], "max_tokens": 1024, "temperature": 0.5, "stream": true }表面看只多了一个"stream": true,但问题在于,Codex 插件压根不发这个字段。如果你不做任何转换,LM Studio 就会以非流式方式返回整个 JSON 响应体,而 Codex 插件的前端解析器,是严格按 SSE(Server-Sent Events)流式格式来消费数据的,它会卡死、超时、报错cc switch local proxy failed while handling codex endpoint /responses。第三重是响应体解析鸿沟。Codex 期望的流式响应是:
event: message-start data: {"type":"message_start","message":{"id":"msg_abc","role":"assistant","model":"claude-3-haiku-20240307","stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":24,"output_tokens":15}}} event: content-block-start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} event: content-block-delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"def clean_data"}} event: content-block-delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"(df):\n return df.dropna()"}}而 LM Studio 的 OpenAI 兼容模式返回的是标准的 OpenAI 流式格式:
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1717023456,"model":"llama3:8b","choices":[{"index":0,"delta":{"role":"assistant","content":"def clean_data"},"finish_reason":null}]} data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1717023456,"model":"llama3:8b","choices":[{"index":0,"delta":{"content":"(df):\n return df.dropna()"},"finish_reason":null}]}两者 event 名称、data 字段嵌套层级、文本块分片逻辑完全不同。插件前端拿到一堆data: {...}却找不到event: content-block-delta,自然无法渲染。这就是为什么所有“Codex 无法加载组织设置”、“Codex is ignoring 1 unrecognized configuration setting”的报错,根源都在这里——插件在尝试解析一个它根本不认识的响应流。
提示:网上流传的所谓“修改 Codex 源码”方案,本质上就是想绕过这层代理,直接让插件发请求给本地模型。这在技术上可行,但代价巨大:你需要 fork 插件仓库、修改其网络请求模块、重新编译打包、手动安装到 VS Code,且每次插件更新,你的 patch 都会失效。这违背了“OpenRig”追求的轻量、可维护、可复现的初衷。正解永远是:在插件和模型之间,插入一个智能的、可配置的、职责单一的协议翻译层。
3. 构建你的 OpenRig 核心:一个 120 行的 Node.js 代理服务详解
既然必须自建代理,那它该长什么样?我的答案是:极简、专注、可调试、零依赖。我拒绝使用 Express、Fastify 这类全功能框架,因为它们带来的抽象层,反而会掩盖协议转换的核心逻辑。我们用原生 Node.js 的http模块,配合stream和events,写一个纯粹的、透明的、可单步调试的代理。整个服务的核心逻辑,就藏在这 120 行代码里(已去除注释和空行,实际可运行代码约 95 行):
// openrig-proxy.js const http = require('http'); const { URL } = require('url'); const { Transform, PassThrough } = require('stream'); // 1. 配置:指向你的本地模型服务 const MODEL_ENDPOINT = 'http://localhost:1234'; // LM Studio 默认端口 const MODEL_PATH = '/v1/chat/completions'; // 2. 创建一个将 Codex 请求体转为 LM Studio 请求体的 Transform Stream class CodexToLMStudioTransformer extends Transform { constructor(options) { super({ ...options, objectMode: true }); } _transform(chunk, encoding, callback) { try { const json = JSON.parse(chunk.toString()); // 关键转换:添加 stream: true,并映射字段 const lmStudioReq = { model: json.model || 'llama3:8b', messages: json.messages || [], max_tokens: json.max_tokens || 1024, temperature: json.temperature || 0.7, stream: true // 强制开启流式 }; this.push(JSON.stringify(lmStudioReq)); callback(); } catch (e) { callback(e); } } } // 3. 创建一个将 LM Studio 流式响应转为 Codex 流式响应的 Transform Stream class LMStudioToCodexTransformer extends Transform { constructor(options) { super({ ...options, objectMode: true }); } _transform(chunk, encoding, callback) { try { const line = chunk.toString().trim(); if (!line.startsWith('data:')) { callback(); return; } const data = line.slice(5).trim(); if (!data) { callback(); return; } const parsed = JSON.parse(data); // 关键转换:提取文本 delta 并包装成 Codex 格式 const choices = parsed.choices || []; if (choices.length === 0) { callback(); return; } const delta = choices[0].delta || {}; const text = delta.content || ''; if (text) { // 构造 Codex 的 content-block-delta event const eventLine = `event: content-block-delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } })}\n\n`; this.push(eventLine); } callback(); } catch (e) { callback(); } } } // 4. 主代理服务器 const server = http.createServer((req, res) => { if (req.method !== 'POST' || !req.url.endsWith('/v1/messages')) { res.writeHead(404); res.end('Not Found'); return; } const proxyReq = http.request({ hostname: new URL(MODEL_ENDPOINT).hostname, port: new URL(MODEL_ENDPOINT).port || 80, path: MODEL_PATH, method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' } }); // 请求体转换:Codex -> LM Studio const transformerIn = new CodexToLMStudioTransformer(); req.pipe(transformerIn).pipe(proxyReq); // 响应体转换:LM Studio -> Codex const transformerOut = new LMStudioToCodexTransformer(); proxyReq.on('response', (proxyRes) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); proxyRes.pipe(transformerOut).pipe(res); }); proxyReq.on('error', (err) => { console.error('Proxy request error:', err); res.writeHead(500); res.end('Internal Server Error'); }); }); server.listen(3000, () => { console.log('OpenRig Proxy running on http://localhost:3000'); });这段代码的价值,不在于它有多炫酷,而在于它精准地击中了协议转换的三个关键点。第一,CodexToLMStudioTransformer类,它不是一个简单的 JSON 字段复制,而是做了语义映射:json.model可能为空,就 fallback 到默认模型;json.max_tokens如果缺失,就设一个安全值。这避免了因上游插件传参不全导致的后端 400 错误。第二,LMStudioToCodexTransformer类,它没有试图去解析完整的 OpenAI 流式响应,而是采用“懒解析”策略——只抓取data:行,只提取choices[0].delta.content,只构造最核心的content-block-delta事件。这种“够用就好”的哲学,极大提升了鲁棒性,即使 LM Studio 返回了额外的、未定义的字段,也不会崩。第三,整个代理的错误处理是分层的:proxyReq.on('error')捕获网络层错误,try/catch捕获 JSON 解析错误,if (!line.startsWith('data:'))过滤掉非数据行。每一层错误都有明确的日志输出,让你在tmux里一眼就能看到是请求发错了,还是响应解析错了。
注意:这个代理默认监听
http://localhost:3000。你必须在 VS Code 的 Codex 插件设置里,把Codex: Endpoint改成这个地址。别忘了,Codex 插件的Codex: Api Key字段此时可以留空,因为你的代理层已经接管了所有认证逻辑,它不再需要向云端发送密钥。
4. 稳定性与可观测性:用 tmux + 日志轮转构建生产级运维看板
一个能跑起来的代理,和一个能长期稳定运行、出了问题能快速定位的代理,中间隔着一条河。很多开发者在本地测试成功后,就把代理进程丢在后台,用nohup node openrig-proxy.js &启动,然后就不管了。结果是:某天早上发现 Codex 不工作了,ps aux | grep node一看,进程没了;或者curl http://localhost:3000/v1/messages返回 500,但控制台一片空白,不知道是 Node.js 崩了,还是 LM Studio 挂了,还是网络断了。这就是缺乏运维看板的典型症状。“OpenRig”之所以被很多人诟病“不稳定”,90% 的原因不在代码,而在运维。
我的解决方案是:tmux + 日志轮转 + 健康检查脚本,三位一体。首先,用tmux创建一个命名会话,专门跑 OpenRig:
# 新建一个名为 'openrig' 的 tmux 会话 tmux new-session -d -s openrig # 在会话的第一个窗格里,启动代理,并将 stdout/stderr 重定向到日志文件 tmux send-keys -t openrig:0 'mkdir -p ~/logs/openrig && node openrig-proxy.js > ~/logs/openrig/proxy.log 2>&1' C-m # 在第二个窗格里,启动一个实时日志监控(tail -f) tmux new-window -t openrig:1 tmux send-keys -t openrig:1 'tail -f ~/logs/openrig/proxy.log' C-m # 在第三个窗格里,启动一个简单的健康检查循环 tmux new-window -t openrig:2 tmux send-keys -t openrig:2 'while true; do curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health || echo "DOWN"; sleep 5; done' C-m # 附着到会话,开始工作 tmux attach-session -t openrig这段脚本创建了三个窗格:第一个是代理服务本身,所有输出都进了~/logs/openrig/proxy.log;第二个是实时日志流,你随时能看到最新请求和错误;第三个是心跳检测,每 5 秒curl一次/health端点(你需要在代理代码里加一个简单的健康检查路由),如果返回非 200,就打印DOWN。这样,你一眼就能看出服务状态。
但光有实时日志还不够,日志文件会无限增长。所以,我用一个极简的 Bash 脚本做日志轮转:
#!/bin/bash # rotate-logs.sh LOG_DIR="$HOME/logs/openrig" MAX_LOGS=7 # 获取所有日志文件,按修改时间排序,保留最新的 MAX_LOGS 个 ls -t "$LOG_DIR"/proxy.log* 2>/dev/null | tail -n +$((MAX_LOGS + 1)) | xargs -r rm # 如果主日志文件大于 10MB,就轮转它 if [ -f "$LOG_DIR"/proxy.log ] && [ $(stat -c%s "$LOG_DIR"/proxy.log 2>/dev/null) -gt $((10 * 1024 * 1024)) ]; then mv "$LOG_DIR"/proxy.log "$LOG_DIR"/proxy.log.$(date +%Y%m%d_%H%M%S) fi把它加入 crontab,每天凌晨执行一次:0 0 * * * /path/to/rotate-logs.sh。这样,你的磁盘永远不会被日志塞满。
最后,也是最关键的,是错误分类与告警。我在proxyReq.on('error')和transformerIn._transform的catch块里,都加入了详细的上下文日志:
proxyReq.on('error', (err) => { console.error(`[PROXY ERROR] Network failure to ${MODEL_ENDPOINT}${MODEL_PATH}:`, err.message); console.error(`[PROXY CONTEXT] Request ID: ${Date.now()}`, 'Upstream:', req.headers['x-request-id'] || 'N/A'); res.writeHead(500); res.end('Internal Server Error'); });这条日志包含了三层信息:错误类型(Network failure)、目标地址(MODEL_ENDPOINT)、唯一请求 ID(Date.now())。当 Codex 报错时,你回到 tmux 的日志窗格,Ctrl+F搜索PROXY ERROR,就能瞬间定位到是哪次请求失败了,失败原因是什么,甚至能关联到上游插件的请求 ID(如果你在 VS Code 里启用了请求追踪)。这种结构化的日志,是高效排障的生命线。它把一个模糊的“Codex 不工作了”,精准地缩小到“第 1427 次请求,因连接 LM Studio 超时而失败”。这才是“OpenRig”真正走向稳定的最后一公里。
5. 实战排障:从 “cc switch local proxy failed” 到 “Codex is ignoring 1 unrecognized configuration setting” 的完整溯源链
现在,让我们把前面所有的理论,放进一个真实的、高频发生的故障场景里,走一遍完整的排障闭环。这个场景,就是你在搜索引擎里输入频率最高的报错:“cc switch local proxy failed while handling codex endpoint /responses. provi”。注意,报错信息被截断了,后面跟着provi,这其实是provider的开头。整句应该是 “cc switch local proxy failed while handling codex endpoint /responses. provider not found”。这是一个典型的、由多层错误叠加导致的“马赛克式”报错,它的根源,往往不在最外层的 Codex 插件,而深埋在代理层或模型层。
第一步:现象确认与初步隔离。你在 VS Code 里打开一个文件,点击 Codex 的“Ask”按钮,几秒后,右下角弹出红色报错框,内容就是上面那句。此时,不要急着 Google,先做三件事:1)打开 tmux 会话tmux attach-session -t openrig;2)切换到日志窗格(Ctrl+B, 1);3)观察是否有新的PROXY ERROR或Transform error日志。如果没有,说明请求甚至没到达你的代理层,问题出在 Codex 插件的配置上。这时,你应该检查 VS Code 设置里的Codex: Endpoint是否真的是http://localhost:3000,以及Codex: Api Key是否被意外填入了某个无效字符串(比如一个空格),导致插件在构造请求头时出错。
第二步:代理层日志深度分析。假设你看到了类似这样的日志:
[PROXY ERROR] Network failure to http://localhost:1234/v1/chat/completions: connect ECONNREFUSED 127.0.0.1:1234这说明你的代理服务在尝试连接 LM Studio 时被拒绝了。ECONNREFUSED是一个非常明确的信号:目标端口上没有服务在监听。这时,你立刻要做的,不是重启代理,而是检查 LM Studio。打开 LM Studio 的 GUI,看右下角的状态栏是不是显示 “Server stopped”。如果是,点击 “Start Server”,并确认端口确实是1234(在 Settings > Server 里可以改)。这个错误,90% 的情况是因为你关机后没手动重启 LM Studio,或者 LM Studio 自己崩溃了。tmux的价值在此刻凸显:你不需要退出当前开发环境,只需Ctrl+B, 2切到健康检查窗格,看到DOWN,就知道该去救火了。
第三步:响应解析错误的精确定位。假设代理日志里没有网络错误,但 Codex 依然报错,且日志里出现了Transform error。你复制出错的那一行日志,发现是:
[TRANSFORM ERROR] Failed to parse LM Studio response: Unexpected token < in JSON at position 0Unexpected token <是一个经典错误,意味着你收到的不是一个 JSON 响应,而是一个 HTML 页面。这通常只有一个原因:LM Studio 的服务器虽然在运行,但它返回了一个 404 或 500 的 HTML 错误页。比如,你把MODEL_PATH写成了/v1/chat/completion(少了个 s),LM Studio 就会返回一个<html><body>404 Not Found</body></html>。你的LMStudioToCodexTransformer在JSON.parse()时,就会遇到第一个字符<,从而抛出这个错误。解决方案极其简单:回到openrig-proxy.js,检查MODEL_PATH的拼写,确保和 LM Studio 文档里写的完全一致。
第四步:配置项忽略的终极真相。最后,那个让人困惑的 “Codex is ignoring 1 unrecognized configuration setting”。这个报错,从来不是 Codex 插件的 bug,而是你(或某个教程)在settings.json里,错误地添加了一个 Codex 插件根本不认识的配置项。比如,你看到网上有人说要加"codex.model": "llama3",但 Codex 的官方配置项里根本没有codex.model这个 key,只有codex.endpoint和codex.apiKey。VS Code 在加载插件时,会扫描所有配置,遇到不认识的,就默默忽略,并在控制台(Ctrl+Shift+P->Developer: Toggle Developer Tools-> Console)里打印这条警告。它不影响功能,只是告诉你:“你写的这个配置,我没用上”。所以,解决方法就是:打开 VS Code 的设置(Ctrl+,),在右上角点击{}图标进入settings.json,搜索codex,把所有非官方文档列出的配置项全部删掉。这个报错,本质上是一个“善意的提醒”,而不是一个“致命的错误”。
经验心得:我踩过的最大一个坑,是在
CodexToLMStudioTransformer里,忘了给json.messages做空值检查。某次 Codex 插件传过来一个空数组[],我的代理把它原样转发给了 LM Studio,结果 LM Studio 返回了一个400 Bad Request的 JSON,里面包含"message": "messages must contain at least one message"。这个 JSON 被LMStudioToCodexTransformer拿去JSON.parse(),然后又因为choices字段不存在而静默失败,最终 Codex 插件收不到任何content-block-delta,就一直转圈,直到超时。修复方案就是在_transform函数里加一行:if (!json.messages || json.messages.length === 0) { callback(new Error('Empty messages array')); return; }。这个教训告诉我:永远不要相信上游传来的任何数据,每一个字段,都要做防御性检查。这是构建任何可靠代理服务的第一铁律。
6. 从 OpenRig 到 OpenStack:如何将你的本地 AI 工作台扩展为团队知识中枢
当你已经能稳定地用 Codex + OpenRig 代理 + LM Studio 写出高质量的 Python 代码时,一个更宏大的问题自然浮现:这套个人工作流,能否升级为一个团队共享的、可协作的、有记忆的 AI 编程平台?答案是肯定的,而且路径非常清晰。我们可以把“OpenRig”看作一个最小可行单元(MVP),而它的自然演进方向,就是“OpenStack”——一个开源的、本地部署的、面向团队的 AI 编程知识栈。
这个演进,不是推倒重来,而是在现有代理层之上,叠加一层“知识增强中间件”。它的核心思想是:在 Codex 的请求到达模型之前,先去查询一个本地的知识库,把相关的上下文(如团队内部的 API 文档、历史 PR 评论、常见 Bug 解决方案)注入到messages数组的最前面。这样,模型在生成代码时,就不再是凭空想象,而是基于你团队的真实知识在创作。实现这个,只需要在CodexToLMStudioTransformer的_transform函数里,加几行代码:
// 在 _transform 函数内部,解析完 json 后,插入知识检索逻辑 async _transform(chunk, encoding, callback) { try { const json = JSON.parse(chunk.toString()); // 1. 从请求的 user message 中提取关键词(例如,用户问 "如何用 pandas 处理缺失值?") const userMessage = json.messages.find(m => m.role === 'user')?.content || ''; const keywords = extractKeywords(userMessage); // 一个简单的关键词提取函数 // 2. 查询本地向量数据库(例如,用 ChromaDB) const relevantDocs = await chromaClient.query({ collectionName: "team-knowledge", queryText: userMessage, nResults: 3 }); // 3. 将检索到的文档,作为 system message 注入到请求体最前面 const enhancedMessages = [ { role: "system", content: `You are an expert developer at our company. Use the following internal documentation to answer the user's question:\n${relevantDocs.join('\n\n')}` }, ...json.messages ]; // 4. 后续逻辑不变:构造 lmStudioReq 并推送 const lmStudioReq = { model: json.model || 'llama3:8b', messages: enhancedMessages, max_tokens: json.max_tokens || 1024, temperature: json.temperature || 0.7, stream: true }; this.push(JSON.stringify(lmStudioReq)); callback(); } catch (e) { callback(e); } }这个改动,带来了质的飞跃。它让 AI 不再是“通用的”,而是“专属的”。你可以把公司所有内部 Wiki 页面、Jira 的史诗级需求描述、GitLab 上每个核心模块的 README,都用chromaClient.addDocuments()导入到team-knowledge这个集合里。当新同事问 “我们的支付网关 SDK 怎么集成?” 时,OpenRig 代理会自动从知识库里捞出三篇最相关的文档,喂给模型,生成的答案,就天然包含了你们公司特有的参数名、回调 URL 格式、错误码含义。这彻底解决了 AI “幻觉”问题——它给出的不是教科书答案,而是你们团队的“标准答案”。
而这一切的基石,依然是那个 120 行的 Node.js 代理。它没有变,只是变得更聪明了。它从一个单纯的“协议翻译器”,进化成了一个“知识调度器”。你不需要更换任何前端插件(Codex 还是 Codex),也不需要重写后端模型(LM Studio 还是 LM Studio),你只是在中间,加了一层薄薄的、可插拔的、用 JavaScript 写的业务逻辑。这就是“OpenRig”这个名字最迷人的地方:它不是一个封闭的产品,而是一个开放的、可生长的、属于你自己的 AI 工作台。它的边界,由你的需求定义;它的能力,由你的代码扩展。当你第一次看到 Codex 用你团队的内部术语,准确无误地写出一段符合规范的代码时,那种感觉,不是在用一个工具,而是在指挥一个真正懂你的伙伴。这,或许就是本地化 AI 编程,最本真的意义。