Vibecoding 这阵子算是彻底火了,我周围的同事、同行几乎人手一个终端窗口,开着 Claude Code 或者 Codex 在那儿"意念编程"。需求说一句,代码哗哗往外冒。但用着用着我就发现一个很现实的问题:这类 CLI 工具天生是"一次性"的,会话绑死在本地终端里,窗口一关,上下文丢了半截,换个电脑更是直接从头再来。尤其是当我开始同时维护好几个项目、还想在平板上随时看一眼进度的时候,原来那套"开个终端就能写代码"的玩法就变得特别别扭。
所以我花了一个周末,做了一个专门给 Claude Code / Codex 用的持久化 Web AI 编码工作区,取名叫 Easy Web Vibecoding。简单说,就是在浏览器里开一个长期存活的工作区,背后由服务端托管claude和codex两个 CLI 的虚拟终端,所有会话、输出、项目上下文都以可持续的方式存下来,换设备、重开机、协作者加入都不会丢状态。这篇文章不聊那种"给你个 Demo 看看"的表面的东西,我把我从零搭起来的完整思路、踩过的坑、以及关于持久化和会话恢复的取舍,全部写出来,希望能给同样想折腾一个趁手 Web 编程环境的同学一些能直接抄作业的参考。
1. 为什么要给 Claude Code / Codex 加一层 Web 工作区
1.1 "Vibecoding" 的本质,以及终端方案的四个痛点
先说说"Vibecoding"这个词。它不是指随便乱写代码,而是指一种由 AI 主导生成、人类只做方向性引导和验收的编程方式:你像在"找感觉"一样描述需求,AI 帮你把具体干活的代码写出来。Claude Code 和 Codex 是最能体现这种工作流的两个 CLI 工具,它们可以直接读仓库、改文件、执行命令,本质上是一个"有仓库权限的 AI 代理"。
但这种模式用多了,你会发现官方 CLI 其实只解决了"AI 能不能干活"的问题,没解决"人的工作流怎么承接"的问题。我列一下用终端直接用它们的四个痛点:
- 会话与终端绑定:Claude Code 之类的工具把会话状态放在本地目录的
.claude等隐藏文件夹里,进程结束后要重新加--resume才能接着聊。换一台电脑,这些状态跟不过来,因为它们是"机器本地"的。 - 上下文碎成一地:我常常上午在公司电脑跑一个 Codex 会话,下午想回家用家里的电脑继续同一件事,结果发现两边上下文对不上,只能把代码提交到 Git,再在新机器上重新描述一遍需求。时间全耗在重复沟通上。
- 输出不可回放、不可检索:终端一滚屏,之前的 AI 长输出就没了。想翻一下 AI 十分钟前给的一条关键提示,你得往上滚 N 屏,还得祈祷终端 buffer 够大。更别说把整段思路分享给同事了。
- 多设备、多人协作没有入口:终端这东西本质上属于"单机独占"。你想让另一个同事"看着你让 AI 干活"的过程,几乎只能靠录屏,或者让他也装一套环境。
这四个痛点的共同指向是:CLI 本身是一个优秀的"代码执行引擎",但它缺一个能长期保存工作状态的"驾驶舱"。所以我想的很简单——我用 Web 技术给这两个 CLI 包一层外壳,让它们跑的虚拟终端和我自己的会话数据库打通,浏览器只做展示,工作区整个放在一台常驻的服务器上。
1.2 Web 工作区到底解决了什么场景问题
有人会问:VSCode 里装个终端插件不也能用吗?我的回答是:VSCode 那套方案是给"你在本地开发"设计的,它的持久化很弱,而且它是桌面应用,没法做到"打开浏览器就能进入我的编码环境"。我做这个 Web 工作区,其实是在模仿云开发环境的思路,但比云 IDE 更轻、更专注:
- 场景一:多设备连续写作。我在公司用 Mac 开着工作区干活,回家后打开家里电脑的浏览器,看到的还是上午那个会话,AI 还在继续等我下一步指令。这个体验很接近"云游戏"——重的东西在远端,你只是换了一块屏幕。
- 场景二:协作者围观与接手。我把工作区的链接发给设计师或产品同事,他们能看到 AI 正在怎么改代码,甚至可以往一个专门的"指令队列"里塞需求。虽然听上去有点科幻,但 Web 端的天然优势就在这里。
- 场景三:长期运行的编码任务。比如库升级映射、批量重构这类耗时任务,我不希望电脑合盖时断掉。Web 工作区跑在服务器上,进程常驻,我只要过一会儿回来看看输出就行。
这几个场景有一个共同隐含要求:工作区必须是"持久化"的——进程可以重启、网络可以断、设备可以换,但会话状态和应用状态不能丢。所以这个项目名字里的"持久化"不是形容词,而是一个技术指标。我后面几乎一半的精力都花在怎么把这两款 CLI 的"短暂生命"拉长成一个"可恢复的工作流"上。
2. 工作区核心架构:PTY 桥接、WebSocket 事件流、会话持久化三层设计
2.1 整体链路:浏览器 → WebSocket → Node.js → PTY → Claude Code / Codex CLI
先给整体架构画个轮廓,免得后面讲细节大家找不到方向。我的服务端用 Node.js 写的,原因是 Claude Code 和 Codex 官方都提供了 Node 生态的调用方式,我用 Node 做桥接最顺,js 生态里也有比较成熟的虚拟终端方案。
一条核心链路是这样的:
- 前端:浏览器里跑一个基于 xterm.js 的终端模拟器,它负责渲染字符流和接收键盘输入。我不会做太复杂的 UI,因为核心是终端。
- 传输层:浏览器和 Node 服务端之间的输入输出用 WebSocket 传输。xterm.js 本身支持
attach到一个 WebSocket 的 socket 上,这给我省了不少事。 - 服务端:Node.js 进程负责两件事。第一,用
node-pty在操作系统层面生成一个伪终端(PTY),把claude或codex的进程塞进去跑;第二,把 PTY 的标准输入输出流转发给 WebSocket,同时把每一行输出写入持久化层。 - 存储层:用一个轻量级数据库存会话元数据和输出日志,文件系统存项目快照和必要的状态文件。
整个链路的"桥接"说白了就是一句话:把 CLI 的 stdin/stdout 从本机终端转移到 Web 端。道理很简单,但落地时要注意 PTY 的尺寸同步、编码处理、以及"断线重连时不干扰 CLI 进程"这几个细节。比如node-pty创建终端时要指定cols和rows,如果 Web 端窗口尺寸变了而不通知 PTY,AI 格式化表格的时候就会错乱。我在代码里每次前端 resize,就会通过 WebSocket 发一条控制消息,服务端调用pty.resize(cols, rows)。
2.2 用 Node-pty 在服务端为 CLI 分配虚拟终端
具体到代码层面,核心其实就是这几行。我创建一个PtySession类来管理一个虚拟终端生命周期,它会处理"启动 CLI 进程""转发数据""关闭清理"三件事:
import * as pty from 'node-pty'; class PtySession { constructor(shellCommand, args, cwd, cols, rows) { this.ptyProcess = pty.spawn(shellCommand, args, { name: 'xterm-color', cols: cols || 80, rows: rows || 30, cwd: cwd, env: process.env, }); this.buffer = ''; this.listeners = []; } onData(callback) { this.ptyProcess.onData((data) => { this.buffer += data; callback(data); }); } write(data) { this.ptyProcess.write(data); } resize(cols, rows) { this.ptyProcess.resize(cols, rows); } kill() { this.ptyProcess.kill(); } }这里有个关键点值得说一下:我是用pty.spawn而不是child_process.spawn,区别在于node-pty创建的是一个真正的伪终端设备,它会给子进程分配一个 controlling terminal。Claude Code 这类工具在启动时会检测当前环境是不是 TTY,如果是 TTY,它会开启交互模式、支持按键绑定、显示进度动画;如果不是 TTY(比如普通管道),它会认为进入了非交互模式,很多功能会降级甚至直接罢工。所以想要"原汁原味"的 Claude Code 体验,就离不开 PTY。
实际运行 CLI 的时候,命令大概是这样的:
// 启动 Claude Code 的 PTY 会话 const shell = '/bin/bash'; const args = ['-lc', 'claude --dangerously-skip-permissions']; const session = new PtySession(shell, args, '/home/user/projects/my-app', 120, 40);--dangerously-skip-permissions这个参数不一定是必须的,看你自己对 AI 直接读写文件的接受程度。我建议是开发工作区可以用,但最好限制一下仓库范围,别让 AI 有权限动工作区外面。
2.3 会话持久化的存储选型:SQLite 与 Redis 的分工
聊到持久化,很多人第一反应就是上 Redis,因为热词里也一直有"redis持久化"这个话题。但我实际做下来,在这个场景里 SQLite 比 Redis 更适合当主存储,Redis 更适合当"事件缓冲层"。我简单对比一下:
| 维度 | SQLite(better-sqlite3) | Redis |
|---|---|---|
| 数据结构 | 表结构清晰,适合存会话元数据、消息记录 | 适合存热数据、时间窗口内的事件流 |
| 持久化可靠性 | 落盘,重启不丢 | 依赖 RDB/AOF 配置,可能要调优 |
| 查询能力 | 支持 SQL,方便按会话/时间检索历史输出 | 查询能力弱,基本靠 key 扫描 |
| 部署复杂度 | 零配置,单文件 | 需要额外起一个服务,增加运维负担 |
| 适合承担的工作 | 会话主存储、录制数据落库 | 输出流缓冲、短期登录态、WebSocket 粘合 |
我最后的方案是:SQLite 为主存储,Redis 只做高频流缓冲。具体分工:
- SQLite 里建几张表:
sessions(会话元数据)、events(每一条终端输出事件)、commands(用户输入的指令)、project_snapshots(项目状态快照)。 - Redis 里放当前活跃会话的"最近一小时的输出流"以及 WebSocket 连接状态,目的是让"恢复一个活跃会话"时不需要全量查 SQLite,直接从 Redis 拿最近的数据快速补屏。
这一步是我后来优化重连体验时加的。最初我全是 SQLite,结果发现 WebSocket 断了重新连接时,如果 AI 已经输出了几千行,前端要一次性拉几千行渲染,体验很卡。后来加了 Redis 做缓冲,重连时先补最近 50 行,然后历史部分再懒加载,手感一下就顺了。
3. 持久化机制的落地:从进程重启到上下文恢复
3.1 CLI 本身只断连不存档,工作区需要自己兜底
Claude Code 和 Codex 这类工具,本质上默认你"在同一个终端里持续聊"。你 Ctrl+C 退出、或者关掉电脑,这个会话确实是存在的(它写在<project>/.claude或者~/.codex里),但恢复要靠手动指令(Claude Code 的/resume、Codex 的--continue),而且只能在原来那台机器上找。这跟我想要的"工作区持久化"相距甚远。
所以我的工作区要做"兜底":不管 CLI 进程是不是还活着,工作区层面都必须把会话相关的状态掌握在自己手里。具体来说,我干了这几件事:
- 用户每次操作(输入一条指令、AI 输出一段文本、AI 修改一个文件)都产生一个事件,事件落 SQLite。
- 每隔一段时间(比如 30 秒)或者每次 AI 有大的输出时,把当前项目的 Git 状态、关键文件内容变更做一个"快照",存在
project_snapshots表里。 - 会话结束后,不删除任何数据。下次从工作区首页点开这个会话,不是直接拉起 CLI,而是先展示这个会话的"历史回放",你可以选择"继续会话"或者"基于这个会话开一个新分支"。
这个设计本质上是我把操作日志(event log)和终端进程解耦了。CLI 进程只是一个"执行器",工作区自己的事件流才是"真相来源"。即便你那台服务器上的 Claude Code 进程崩了、机器重启了,只要 SQLite 文件在,所有对话记录都能原样恢复出来。
有一个细节大家容易忽略:Claude Code 的会话恢复文件有自己的格式,直接复制到新机器不一定认。所以与其去 hack 它的私有格式,不如自己把原始输入输出流都记录下来。我恢复会话时,要么让 CLI 用自己的/resume去加载它自己认识的上下文,要么你干脆重新发起一个带"摘要指令"的新会话,把之前的决策记录通过系统提示喂回去。前者适合短期恢复,后者适合跨机器、跨工具的上下文迁移。
3.2 录制/回放:把每一次 AI 输出变成可检索的历史
持久化最大的价值不只是"不丢",而是让过去发生过的每一件事都能被随时翻出来。我用 SQLite 的events表记录每一次输出事件,效果类似给 AI 编程过程装了一个"黑匣子":
CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, event_type TEXT NOT NULL, -- 'user_input' / 'ai_output' / 'file_change' / 'system' content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX idx_events_session ON events(session_id, created_at);然后我在 Web 工作区里做了一个简单的"回放模式":以时间轴的形式把一次会话从开始到结束的所有输入输出展示出来,支持搜索关键词。比如你记得 AI 某个时候提到过一个函数名retryWithBackoff,但忘了它具体改了哪个文件,直接在回放里搜一下,上下文的来龙去脉就全出来了。
这段代码非常简单,就是查表然后渲染,我不贴了。但我要强调一下设计意图:回放功能做的不是"录屏",而是结构化的事件序列。录屏文件很大、不可检索,而事件序列天然就是文本,既能搜、又能作为后续"上下文压缩"的原料。这条思路在你做任何"AI 工作流持久化"工具时都通用。
3.3 模型上下文摘要持久化的设计
说到上下文,我遇到过另一个头疼的问题:Claude Code 或者 Codex 在长会话里,上下文窗口会慢慢被堆满。它们自己有压缩机制(比如 Claude Code 的compact),但压缩完之后你和 AI 对话的"连续性"其实会有损失。在这个场景上,我的持久化层能帮上一个大忙:
我可以在每次会话结束时,用 CLI 输出的全部对话记录,让模型生成一份结构化的"会话摘要",放进 SQLite。下次你要在另一台设备上继续这个任务时,工作区会先给你看这份摘要,然后你可以选择"按摘要发起新会话",把摘要作为初始提示的一部分喂给 Claude Code 或 Codex。
这样做的核心价值在于:模型原生的上下文压缩是它自己的内部行为,外部看不到压缩结果;而我的摘要持久化是工作区层面显式保存的,随时可见、可修改、可复用。比如我在跑一个库升级任务时,第一天完成了依赖分析和风险清单,第二天早上我只需要在新会话里让 AI"读取刚才的摘要,从第二步开始继续",它就能无缝接着干。这在纯终端方案里很难做到,因为第二天你得重新加载那个可能已经超长、被压缩过、或者根本找不回来的会话。
存储层方面,我记得热词里有人专门搜 redis 持久化机制,说明大家在做一个带状态的系统时,普遍关心"重启会不会丢"。我用 SQLite 做主力之后,重启问题几乎不存在了,因为它是落盘的。不会出现 Redis 默认配置下"只存内存"导致重启全部清空的坑。如果非要在项目里引入 Redis,请一定确认 AOF 或 RDB 的持久化策略是开启的,否则你所谓持久化就是假的,一重启全没。
4. 安装、配置、接入模型的完整实操
4.1 Claude Code 与 Codex 的安装与认证,避开组织权限坑
既然是给这两款工具做工作区,首先得把它们装好、认证过。安装本身不复杂:
- Claude Code:通常用 npm 全局安装,或者用官方安装脚本。装完第一次运行会引导登录,让你关联 Claude 账号。如果你是用订阅账号,需要注意组织权限的问题——热词里那句"your organization has disabled claude subscription access for claude code"其实就是这个坑的典型报错。我的处理方法是:在工作区里提供环境变量配置区,让用户明确指定是用个人订阅还是走 API Key,避免 Claude Code 自己探测到组织上下文,然后被组织的策略卡住。可以在启动命令前显式设置
CLAUDE_CODE_USE_API=1配合 ANTHROPIC_API_KEY 使用。 - Codex:OpenAI 的 CLI 工具,安装后需要登录 OpenAI 账号拿到凭证。Codex 的认证文件会存在
~/.codex/auth.json这类位置,如果启动时报 "auth token is unavailable",多半是凭证没写入或路径不对。工作区服务端要做到"把用户配置好的凭证文件路径挂进 PTY 的环境变量里"。
如果你跑在服务器上,记得把这两个工具的认证状态放到一个只有工作区进程能访问的目录里,别不小心把 API Key 打进日志。我踩过一个坑:node-pty默认继承process.env,如果服务端配置里不小心暴露了ANTHROPIC_API_KEY,而你的日志组件把 env 全量打印了,就等于把密钥发出去了。建议打印 env 之前先过滤掉含KEY、TOKEN、SECRET的字段。
4.2 代理与本地端点配置,处理 cc switch local proxy failed
这里我想专门讲一个高频报错:cc switch local proxy failed while handling codex endpoint /responses。我最初看到这个报错是在自己试一些本地配置时,后来帮朋友排查也遇到一模一样的问题。它的本质是:Codex CLI 把API请求指向了一个本地代理端点,但这个代理转发/responses这个路径时失败了。
排查链路值得大家记住:
- 先确认 Codex 的配置文件里是不是设置了
base_url或代理相关字段。如果指向http://127.0.0.1:xxxx,那就是走了本地中转。 - 再看这个"本地代理"进程到底起来没有。很多时候是代理进程挂了或者没启动,Codex 请求过去直接 connection refused。
- 确认代理进程是否实现了 OpenAI Responses API 规范。
/responses是 OpenAI 新式接口的路径,如果你的代理只实现了旧的/v1/chat/completions,握手就会失败,报错就是这个。 - 确认代理进程有没有正确的 TLS 校验配置。代理如果用了自签名证书,Codex 默认会校验证书链,报错也可能是 TLS 层面的。
我在工作区里针对这种情况做了一件事:提供一个"本地端点探测"按钮。用户在设置里填好 base URL 之后,工作区会主动发一个最小化的请求测试连通性,把具体的报错原因展示出来(是连接失败、超时、还是路径不符合规范)。不需要等你真正跑 Codex 才暴露问题。
我建议所有要在服务器上折腾这类工具的同学,先把代理和模型端点的连通性做成一个独立的诊断步骤。这能帮你省掉大量"AI 工具启动后静默失败"的排查时间。
4.3 接入 DeepSeek 和 LMStudio 本地模型的配置样例
热词里很多人搜"claude code 调用 lmstudio 的本地模型""codex 接入 deepseek",说明大家已经不只是想用官方模型,而是希望把这类 CLI 工具接到各种模型源上。这块每个版本配置方式会有差异,但大原则一致:CLI 在启动时允许你指定 base URL 和模型名称,你把请求路由到自己想要的端点就行。
最典型的两个例子:
- DeepSeek 接入 Codex:如果你想让 Codex 走 DeepSeek 的 API,核心是把 Codex 的请求 base URL 改到 DeepSeek 兼容端点,然后把模型名设为 DeepSeek 对应的模型 ID。具体来说就是修改 Codex 的配置文件或启动参数,指向
https://api.deepseek.com这类地址,认证用 DeepSeek 的 API Key。需要注意 DeepSeek 的接口在"是否完整兼容 Responses API"上可能有限制,如果报/responses不支持,就得找它是否有兼容转换层。 - LMStudio 本地模型:Claude Code 走本地模型,一般就是设置
ANTHROPIC_BASE_URL=http://127.0.0.1:1234/v1(LMStudio 默认端口),然后绕过认证设置(可能设个假 key 就行)。本地模型跑起来的好处是省钱、隐私好、不会有海外 API 的延迟问题,但问题是 Claude Code 对模型能力要求比较高,本地小模型经常会出现工具调用格式错误,导致 AI 明明想改文件却执行不了。
我给的通用建议是:如果你只是想测试流程,先用官方模型跑通,再换本地模型。否则你可能分不清问题是出在"工作区代码"还是"模型能力不足"。我自己试过把 Claude Code 接一个很小的本地模型,结果 API 格式都对,但模型经常漏掉工具调用的参数,最后我只能得出结论:vibecoding 还是得配一个能打的大模型。
5. 我在实际使用中踩过的坑与完整的排查链路
5.1 认证报错的排查顺序:auth token 缺失与组织订阅禁用
先说两个几乎人人都遇到的认证报错。第一是 Codex 的auth token is unavailable,第二是 Claude Code 的组织订阅被禁用。我的排查顺序是这样:
- 先确认 CLI 进程启动时的 HOME 环境变量是谁的。Codex 找 token 依赖
~/.codex/auth.json,如果你用 pm2、systemd 或 docker 启动服务端,HOME 可能指向一个奇怪的位置,导致它找不到凭证。排查方式:在 PTY 里手动echo $HOME,再看对应目录下有没有凭证文件。 - 再确认凭证文件的权限,有些工具在新版本里要求 token 文件权限必须严格 600,否则直接拒绝加载。
- 如果 Claude Code 报组织禁用订阅,先看它是不是识别到了组织上下文。可以显式设置环境变量,比如用 API Key 模式绕开订阅校验。注意别在代码里硬编码组织 id,让用户配置。
- 最后才是检查网络连通性。API 域名能不能连通、证书链完不完整。如果 API 请求走到半路超时,报错也会非常晦涩,但不要第一步就怀疑网络,先排除配置。
5.2 大上下文下的 PTY 缓冲区与 WebSocket 性能问题
我实际连续跑了几周 Claude Code 做真实项目,发现一个之前没预料到的瓶颈:当一个会话非常长时,PTY 的输出量非常大,WebSocket 转发和 SQLite 写入会互相抢资源。CLI 一次输出几千行日志,onData回调频繁触发,如果每条都直接写 SQLite,写入速度跟不上,会造成终端输出卡顿。
我的解决方案是在中间加了一层"批量写入队列":
let pendingEvents = []; setInterval(() => { if (pendingEvents.length === 0) return; const batch = pendingEvents; pendingEvents = []; insertBatch(batch); }, 300);每 300 毫秒批量写一次,写入量立刻降下来。同时 WebSocket 发给前端的消息不经过批量队列,保证实时性。如果你也做一个类似的实时工具,记得把"实时通道"和"持久化通道"在代码层面分开,不要因为写盘太慢把实时交互也拖垮了。
5.3 Web 工作区自身的权限与安全边界
Web 工作区能给远程机器上的 AI 编码,能力越大风险越大。我把安全边界分成了三层:
- 第一层:认证。工作区必须登录才能访问。我用的方案是服务端签发短期 JWT,WebSocket 连接时校验 token,Token 过期就强制重连。
- 第二层:目录隔离。每个项目工作区只能访问自己分配的工作目录,我服务端启动
claude时强制将cwd指向该项目目录,不在 shell 里让用户自由 cd。防止有人通过 AI 的终端能力摸到服务器其他目录。 - 第三层:操作审计。所有执行过的命令、AI 修改过的文件都会写进 SQLite。出了事能回溯到底是谁、在哪一步、让 AI 做了什么事。
我特别想提醒的一点是:Claude Code 和 Codex 都有执行 shell 命令的能力,这意味着谁拿到了你的 Web 工作区,几乎等于拿到了一台服务器的交互 shell。所以千万别把这个服务暴露在公网裸奔,前面至少套一个反向代理做基本认证,甚至加上 IP 白名单。我自己是把它放在 Tailscale 内网里,只有自己几台设备能访问,这个安全模型比"加个密码"要靠谱得多。
最后再分享一个我实际操作中小技巧:不要试图让 Web 工作区去模拟 Claude Code 或 Codex 的全部交互特性,那会让你陷入无穷无尽的兼容性工作。你只需要做好三件事——稳定的 PTY 转发、可靠的会话持久化、干净的 WebSocket 重连,其余的功能保持朴素,这个工作区就已经比裸终端好用太多了。我用了三个星期做真实项目,现在日常开发已经离不开它了。