
1. 为什么需要一个“持久化 Web 工作区”1.1 vibecoding 玩久了终端就成了瓶颈这两个月我把 Claude Code 和 Codex 从终端窗口里拽了出来塞进浏览器还让每个 AI 编码会话都能在机器重启、网络断开之后原样复活。这个项目我起名叫 Easy Web Vibecoding一个专为 Claude Code / Codex 打造的持久化 Web AI 编码工作区。它解决的正是 vibecoding 时代最膈应人的问题——上下文太容易丢。终端一关刚才让 AI 改了十几个文件的思路全没了换台电脑昨天的会话找不回来想拉同事一起看他改的代码只能截图发到群里。项目思路很朴素把这类命令行编码工具包一层 Web 服务用 WebSocket 桥接程序标准输入输出再用 Redis 把消息、命令历史、工作区状态全部落盘。适合正在重度使用 Claude Code、Codex 或同类 AI 编程 CLI 的人也适合想在浏览器里给 AI 编码加一层协作界面的小团队参考。先说痛点。我用 Claude Code 和 Codex 干活的日常是这样的早上开终端进项目目录跑一条任务AI 开始读文件、改代码、执行命令我一边看输出一边追加需求。听起来很顺但 AI 编码会话有个独有麻烦——它的状态完全绑在那一次终端进程里。你关掉 terminal新开一个窗口claude 或 codex 里的对话历史、工具调用记录、已经做了一半的修改全都和你说再见。尤其中午电脑休眠、出差路上网络断掉导致远程会话失联之前改了十个文件、跑了三次测试现场直接归零。这种体验会让人产生一种错觉AI 记性比我还差。其实不是它差是终端这个载体根本没有给“持久化”留位置。第二个痛点是跨设备。Vibecoding 的节奏往往是先让 AI 把框架搭好路上用手机再想想还能加什么需求。可 CLI 工具都长在本地手机上只能看不能动。第三个痛点是协作。AI 干活的时候你想让同事“来看一眼它改了什么”终端里做不到只能录屏或者截图。Easy Web Vibecoding 一开始的目标就不是做一个好看的终端模拟器而是把 AI 编码会话变成可以被保存、被恢复、被多人访问的东西。1.2 为什么不直接用桌面版或 IDE 插件现在 Claude Code 和 Codex 都有桌面版入口VSCode 里也能装插件为什么不直接用我试过。桌面客户端解决的是“单机体验”它把会话从原始终端搬到图形窗口但会话还是活在一台机器、一个用户的登录态里。我要的是一层“服务化”的能力浏览器打开就能用后端统一管理进程会话数据落到 Redis多设备访问同一个工作台。用个不恰当的比喻桌面版像一个很好用的收银台我要的是把整个店铺开成 7×24 小时营业的服务收银台只是其中一个组件。插件方案的另一个问题是很“黏编辑器”。VSCode 插件帮你把 claude 命令嵌进 IDE 面板本质上还是本地 Shell 的图形化包装。它不会替你解决多用户、多会话、设备间同步这些东西。团队里其他人要看 AI 的产出还是得各自装一套环境。而我想要的架构是一个人在后端把 Claude Code / Codex 进程跑起来其余人只需要打开一个 URL 就能参与 review、看实时输出、甚至追加需求。这样即使某个人没有配置 API Key也不影响围观和讨论。1.3 方案轮廓浏览器、进程桥、持久化三层整体结构不复杂。最外层是浏览器页面我用 xterm.js 这类终端模拟组件渲染 AI 的流式输出再加左侧会话列表和中间输入框。中间层是一个常驻的 Node 服务收到浏览器 WebSocket 消息后把内容通过子进程的标准输入 pipe 给 claude 或 codex再把它们标准输出上的增量回传给浏览器。最底层是两件存储工作目录直接落在宿主机磁盘上对话和状态交给 Redis。选 WebSocket 而不是普通 REST原因很直接AI 编码 CLI 的输出是持续流式的一个任务可能跑几十秒甚至几分钟REST 的请求-响应模型会逼着你做轮询也不好做服务端主动推送。WebSocket 全双工天然匹配“人发一句话、AI 回一堆流式输出”的场景。而且页面关掉之后连接断开不影响后台进程继续跑这个特性直接决定了“持久化工作区”能否成立。2. 核心架构拆解从浏览器到命令行的闭环链路2.1 三个模块各管一摊我把项目拆成三个模块。第一个是 web-app负责静态页面、API 路由、WebSocket 接入和 JWT 鉴权。第二个是 bridge这是最烧脑的地方它负责 spawn 一个全新的子进程把浏览器发来的文本作为输入写进子进程 stdin把子进程 stdout、stderr 的增量缓冲后广播给浏览器。第三个是 store封装 Redis 客户端所有会话相关的写入和恢复都走这里。三个模块之间不互相调用内部对象只通过事件传递数据后面想把 bridge 拆成独立服务也不会伤筋动骨。bridge 层有两个容易被忽略的细节。第一是进程生命周期一个浏览器标签页关掉不能让正在跑到一半的 CLI 被 kill否则改到一半的工作流全没了。我的做法是子进程由服务端持有和某个 WebSocket 连接解耦页面关掉只是断开推送进程继续在后台跑用户下次打开还能看到实时状态。第二个细节是会话隔离每一个 WebSocket 连接进来我都先查它携带的 sessionId再决定把它绑定到哪个已有进程还是新起一个进程。sessionId 是持久化的基础也是后面做恢复的钥匙。2.2 持久化到底存什么很多人一听持久化就以为只存聊天记录那格局小了。AI 编码工作区里最值钱的是整个上下文你输入了什么AI 说了什么它执行了哪几条 shell 命令改动了哪些文件当前目录里有什么新产出。我设计的数据结构如下session:{id}:messages用 Redis List 存每个会话的消息事件一条消息是一个 JSON 字符串字段包括 role、content、type、timestamp。session:{id}:state用 Hash 存会话的元信息比如 current_dir、model、agent_type、resume_id。workspace:{id}:snapshot用 String 存最近一次关键节点的变更摘要给恢复后的人看当前进度。Redis 的 key 过期时间我没设因为编码会话没有“自动清理”的说法用户手动删除才算数。这样设计的收益是恢复一个会话只需要两步重新 spawn 一个 CLI 进程让它恢复到对应工作目录再把近端历史消息重放给浏览器。AI 自身的对话上下文用 CLI 的 session resume 能力找回claude 的--resume、codex 的resume子命令就是干这个的。工作区文件不用放进 Redis目录还在磁盘上这是最可靠的持久化。2.3 Redis 持久化机制详解这次不裸奔既然用 Redis 存会话Redis 自己挂了就尴尬了。默认配置下 Redis 只把数据放在内存里服务一重启所有会话路标全部清空——相当于我们解决了 CLI 会话丢失结果把数据丢到了 Redis。所以必须开持久化。Redis 持久化主要有 RDB 和 AOF 两条路。RDB 是周期性把内存里的全量数据快照写到磁盘恢复快但两次快照之间的数据会丢。AOF 则是把每一次写命令追加到日志文件恢复时重放日志理论上可以把数据丢失窗口压到秒级。对于 AI 编码会话这种“丢一条消息都可能丢思路”的场景我选 AOF并且把 appendfsync 设成 everysec每秒钟强制把缓冲区里的日志刷到磁盘一次最坏只丢一秒的写入。代价是写放大比 RDB 高但开发机和小团队场景完全扛得住。另一个坑是 AOF 日志膨胀。跑了一周之后appendonly.aof 可能膨胀到几百 MB因为里面记满了早就不需要的中间消息。Redis 7 之后有 AOF 自动重写机制会在后台把日志压缩成只包含当前数据集的最小序列。我在 redis.conf 里会留意auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb这几个默认项如果工作区非常活跃建议单独跑一次BGREWRITEAOF。恢复流程不复杂启动 Redis 时它检测到 appendonly.aof 存在会自动加载真正意味着“写进去就丢不了”。持久化方式核心原理优点缺点适用场景RDB定期生成全量快照文件文件紧凑、恢复快两次快照之间数据可能丢可接受分钟级丢失的缓存场景AOF追加每条写命令启动时重放数据完整性强可秒级恢复文件大、恢复比 RDB 慢对话、命令历史等必须保住的数据AOFRDB 混合用 RDB 打底AOF 记录增量兼顾恢复速度和完整性配置与理解成本高生产环境默认推荐3. 关键实现把 Claude Code 和 Codex 装进 WebSocket3.1 先写一个能挂到 WebSocket 的进程包装器核心代码用 Node.js 写。npm 上 ws 这个库足够稳定配合child_process.spawn就能搭出雏形。我不直接 exec因为 exec 会把 stdout 累积到内存、一次性返回AI 编码任务跑十分钟会直接爆内存spawn 是流式的stdout 一来我们就能转发。调用参数不复杂spawn(claude, [], { cwd: projectDir, env: { ...process.env, ANTHROPIC_API_KEY: userKey } })。对 codex 也一样把命令换成 codex再把环境变量换成对应模型的 key。这里有个容易忽略的点设置shell: false绝不能开 shell 拼接命令否则用户输入里带个分号、管道符就会出安全问题。const { spawn } require(child_process); function startAgent({ command, cwd, env }) { const child spawn(command, [], { cwd, env: { ...process.env, ...env }, shell: false, }); const session { id: ${command}-${Date.now()}, child, online: false, }; child.stdout.on(data, (chunk) { broadcast(session.id, { type: stdout, content: chunk.toString() }); }); child.stderr.on(data, (chunk) { broadcast(session.id, { type: stderr, content: chunk.toString() }); }); return session; }这套骨架看起来不起眼但后面所有能力都从这里长出来。浏览器发来一个{ type: input, content: 帮我改一下登录模块加上错误提示 }bridge 就在 child.stdin 上写一行AI 的输出自动广播回去。难点不在这一段而在于你怎么管理大量同时存在的进程、怎么在网络断开之后不丢数据。3.2 会话恢复页面刷新不等于从头再来恢复的重点有两个重新拉起 CLI 进程以及让页面重新拿到历史上下文。第一步从 Redis 查出要恢复的session:{id}:state拿到原来的工作目录、agent 类型、模型名以及 CLI 自己的 session id。第二步重新 spawn CLI如果 claude 支持--resume就带着原 session id 启动codex 同理这一步相当于告诉 AI 自己“我们刚才聊到哪儿了”。第三步从 messages List 取出最近 N 条消息重放到浏览器这样你不光能看到 AI 自己记得什么还能看到它上线之前我们聊过什么。注意重放只需要发到页面不要重新灌进 CLI 的 stdin否则等于把历史命令又执行了一遍后果很酸爽。我在这里踩过一次恢复了某个会话之后AI 又把之前跑过的构建命令全部跑了一遍那个项目的 CI 直接被打爆。后来我把“浏览器展示”和“进程回放”两个动作彻底拆开才真正稳定。3.3 密钥与多用户隔离多用户场景里最大的隐患是 API Key 全部塞给前端。我第一次快速实现时把 key 写进登录接口的返回值结果浏览器 localStorage 一抓一个准。正确做法是浏览器只传一个 sessionIdkey 存在服务端启动子进程时才注入环境变量。每个用户的工作目录也要隔离我直接拿 userId 做目录后缀避免两个人共用一个项目目录互相踩文件。认证层用 JWT签发时把 userId 和 plan 带上WebSocket 握手时校验 query 参数里的 token。还有一个细节值得记下来如果是以codex login这样的交互式登录来存 token无头服务器上经常读不到缓存新起的子进程就会报类似auth token is unavailable的错误。这时候改用环境变量注入 API key 是最稳的bridge 每次 spawn 的时候把 key 带进去保证进程一出生就有完整凭证。Claude Code 也一样与其依赖它自己的凭据管理器不如在服务端统一管理 key 的注入和轮换。4. 从零搭建Easy Web Vibecoding 全流程实操4.1 准备清单与目录结构动手之前确认三样东西Node.js 18 以上我用 20 LTS 很稳、Redis 7 或更高、本机已经装好 claude 和 codex 命令。还要准备一个 Docker 环境不是必须但用 Compose 起 Redis 比本机安装省心得多。我给项目设计的目录结构不长easy-web-vibecoding/ ├── server/ # Node 服务HTTP WebSocket ├── bridge/ # 进程包装器直接接触 claude / codex ├── store/ # Redis 读写封装 ├── web/ # 静态页面和终端组件 └── docker-compose.yml先把目录建出来再逐个文件填内容。这里不用急着把前端做成一个完整 IDE一个能显示流式输出的终端盒子加上一个输入框已经能覆盖核心链路。4.2 五分钟跑通最小实例依次执行这些步骤npm init -y初始化项目npm i ws express redis jsonwebtoken装依赖在 server 里写一个最小 HTTP 服务用 docker-compose 把 Redis 拉起来最后node server/index.js启动应用浏览器打开http://localhost:8787。# docker-compose.yml services: redis: image: redis:7-alpine command: [redis-server, --appendonly, yes] volumes: - redis-data:/data app: build: . ports: - 8787:8787 environment: - REDIS_URLredis://redis:6379 depends_on: - redis volumes: redis-data:WebSocket 接入的核心代码长这样它做的事情和前面 bridge 的封装对应起来握手时取 sessionId有历史进程就绑定没有就新建。const express require(express); const http require(http); const WebSocket require(ws); const app express(); const server http.createServer(app); const wss new WebSocket.Server({ server }); wss.on(connection, (ws, req) { const url new URL(req.url, http://localhost); const sessionId url.searchParams.get(sessionId); if (!sessionId) { ws.close(); return; } // 绑定已有进程或创建新进程 attachOrCreateSession(sessionId, ws); }); server.listen(8787);这个最小实例能解决什么问题呢它能让你在浏览器里跑通一条完整链路输入需求 → WebSocket 传到 Node → Node 启动 claude → AI 输出流式回到页面。跑通之后再往里面加会话列表、历史回放、多用户权限都很自然。4.3 把 Claude Code 和 Codex 正式接进来把两个命令行工具接进来核心就是环境变量和 resume 参数。Claude Code 需要ANTHROPIC_API_KEYCodex 需要对应的 OpenAI 系 key如果你用的是别的模型端点就按 provider 配置覆盖 base_url。启动时带上各自支持的 session 恢复参数比如claude --resume id或codex resume id。这些细节在 bridge 里都应该作为配置项存在不要把命令路径和平台信息写死在代码里。# Claude Code 接入示例 export ANTHROPIC_API_KEY你的密钥 claude --resume session-id # Codex 接入示例 export OPENAI_API_KEY你的密钥 codex resume session-idCodex 最方便的地方是它对 OpenAI 兼容端点支持很干脆。如果你想把底模切到 DeepSeek就在 provider 配置里加一个块把 base_url 指向它的兼容端点模型名填对就行。Claude Code 更适合 Anthropic 兼容接口如果你想用它接本地模型先确认本地服务暴露的是不是 Anthropic 兼容协议如果只有 OpenAI 兼容协议要么走转换层要么这个问题就交给 Codex 那一侧解决。4.4 接入本地模型LM Studio 与自定义端点本地模型接入是我后来才加的功能。LM Studio 这类工具能起一个 OpenAI 兼容的本地服务端口类似http://localhost:1234/v1。Codex 的 provider 配置里新建一个模型地址指向它再填模型 ID就能完全离线操作。好处很明显模型选择权留给自己API 成本归零代码内容不出机器适合隐私敏感和调试场景。坏处也明显本地小模型在复杂代码生成上的能力上限摆在那里我会保持“快速验证”的心态真干重活用云上模型。接入本地模型时bridge 层基本不用改因为它只负责把文本从 WebSocket 搬到子进程 stdin再把 stdout 搬回来。真正要改的只是 spawn 时传给子进程的环境变量和配置。这种解耦让项目后面再加新工具变得很容易加一个“运行时配置”页面给用户选模型剩下的事情 bridge 都会自行处理。5. 常见问题与排查技巧实录5.1 高频问题速查表把实际使用中碰到最多的情况整理成一张表方便直接对照。现象根因处理方式codex 启动时提示 auth token is unavailable无头环境没有交互式登录缓存或缓存不可读改用 OPENAI_API_KEY 环境变量注入或先交互式登录一次Codex 调用端点时返回 /responses 相关错误配置的 base_url 不支持 OpenAI Responses API 全量路由或网关没有转发该路径换成官方 API base或使用支持该端点的兼容服务自查路由浏览器页面关掉后后台进程被回收WebSocket 断开逻辑里把子进程也 kill 了让子进程生命周期与页面解耦只断推送不断进程Redis 重启后所有会话找不回来appendonly 未开启数据只活在内存里开启 AOFappendfsync everysec确认 appendonly.aof 已落盘恢复会话后 AI 重复执行之前的命令恢复历史时把消息重新输给了 CLI stdin历史消息只重放到页面不塞给进程Claude Code 报订阅或组织访问被禁用账号权限变更、订阅绑定失效、企业策略限制回到官网确认账号状态检查企业策略和许可绑定输出乱码、JSON 解析错乱stdout/stderr 混流、编码不对给两个流打上独立标签统一 UTF-8避免杂项输出混入排查路径说几个通用的。第一所有能看到的现象背后都先怀疑“环境变量是否完整”因为 bridge 用 spawn 起子进程时只继承了自己进程的 env如果你在别的终端里手动 export 的 key这里根本感知不到。第二恢复类问题先看 Redis 里有没有数据redis-cli手动查一下 key 就能判断是没写入还是读取失败。第三凡是和 WebSocket 相关的怪问题先看心跳加了 ping/pong 之后再观察断线重连稳定了很多。5.2 印象最深的三个坑第一个坑是进程残留。最初我直接在 WebSocket close 事件里执行child.kill()结果 claude 的子进程、它启动的 shell 子进程全都成了孤儿。后来改成用process_group统一管理kill 的时候连整个进程组一起清掉。这个坑不亲自踩一次很难意识到你以为 kill 掉的是一个人实际它是一个家庭。第二个坑是输出缓冲。有个会话跑到一半页面突然不动了排查半天发现 claude 自己输出的状态文本和返回给 UI 的真实内容混在一个管道里前端解析 JSON 时直接崩掉。解决方式是在 bridge 里给 stdout 和 stderr 打上不同前缀前端各走各的渲染通道。输出类问题不要试图靠肉眼盯该做的结构分离必须做。第三个坑是关于密钥的。我有一版把 API key 写进了某个供下载的配置文件中打算“这样用户拿到就能跑”结果被内部安全扫描拦下来。实际改了之后所有 key 一律通过环境变量注入并加了一层简单的轮换机制。密钥管理这件事宁可重做一遍也不要留侥幸。5.3 后续扩展和一点收尾这个项目目前能跑下一步的扩展方向我列三个一是接入 MCP让 claude / codex 在 Web 工作区里能操作更多外部工具二是给每个会话生成一个只读分享链接方便团队 review三是把工作目录做成模板一键拉起一个带初始代码结构的新项目。每一个扩展都不需要动 bridge 和 store 的核心只要加 API 和页面入口。最后说点个人体会。做 Easy Web Vibecoding 最大的收获不是代码写得多漂亮而是切身体会到“工具链的可组合性”。Claude Code 和 Codex 是很好用的单点工具但它们缺一个服务化的壳。这个壳的每一层都是成熟技术WebSocket、子进程、Redis没有任何魔法。真正花时间的全是边界情况进程生命周期、断线重连、历史消息与进程输入之间的错位。把这些边界理顺之后AI 编码这件事才从“一个终端里的事”变成了“一个团队可以围绕它建流程的事”。这套方案也许不是最优解但它提供了一个很实在的起点终端工具不该再是一次性的临时会话它们完全有资格成为一个有记忆、可访问、能协作的工作区。