☰
持久化Web AI编码工作区:集成Claude Code与Codex实战
2026/10/1 6:58:03 网站建设 项目流程

最近一直在折腾 AI 编码工具,Claude Code 和 Codex 都让我非常上头——一个命令直接进交互式终端,几句自然语言就能改代码、跑测试、修 bug,效率确实高。但用久了就发现一个很现实的问题:这俩工具默认都是“贴着终端走的”,换个窗口、重启一下机器,之前的对话上下文、工作目录切换、临时配置基本就全没了。对于我这种同时管四五个项目的场景,这体验实在有点难受。

于是我就动手搞了一套Easy Web Vibecoding——一个专门为 Claude Code / Codex 打造的持久化 Web AI 编码工作区。简单说,它把原本只活在本地终端的 AI CLI 搬进了浏览器,所有会话、输出、历史记录都会持久化保存,随时可以新建、暂停、恢复,哪怕服务器重启,之前的项目状态还在。这篇文章会把我搭建这套工作区的完整思路、核心代码逻辑、踩坑记录和扩展方案都写下来,不管你是刚入门的 AI 编码新手,还是已经在大量使用 CLI 的老司机,都能从中找到可以直接复用的经验。

1. 项目概述与核心思路

1.1 什么是 Easy Web Vibecoding

“Vibecoding”这个词最近在开发者圈子里很火,大意是指“顺着灵感写代码”——不用先写详细设计,而是通过自然语言向 AI 描述意图,让 AI 直接生成实现。Claude Code 和 Codex 正是这类工具的代表:它们以 CLI 交互终端的形式运行在本地,用户可以像聊天一样下达任务,AI 自主完成文件编辑、命令执行、测试运行等工作。

Easy Web Vibecoding 不是一个替代品,而是一个“外壳”。它解决的是这两个 CLI 工具使用场景逼仄的问题:原来你只能在某个特定的终端窗口里启动它们,关闭窗口进程就没了;现在你可以在浏览器里打开一个工作区,只要服务还在,AI 编码会话就在。你甚至可以同时打开多个项目页,每个页面对应一个独立的 AI 会话。这个工作区会记录下每一次输出的原始内容、执行时间、所在目录和上下文备注,方便你随时回溯。

我的目标很明确:让 AI 编码工具从“一次性临时任务”变成“可长期维护的工作现场”。在这个工作区里,你可以把 Claude Code 和 Codex 当作两个可切换的“引擎”,也可以只认准其中一个。所有配置都存放在同一个环境文件里,不会互相污染。

1.2 为什么需要持久化 Web 工作区

先说痛点。我一开始直接裸用 Claude Code 和 Codex,遇到的最大问题就是会话状态丢失。举个例子:某个下午我在项目 A 里让 Claude 写一个解析器,写了一半因为要响应临时需求切到项目 B 去修复 bug;等你再切回项目 A,原来的对话上下文已经滚出终端缓存,想恢复必须靠--continue回溯最近一次会话。但如果中途重启过机器,或者不小心关掉了终端窗口,连--continue也无能为力了。

第二个痛点是多项目并行时的混乱。每个项目都要开一个独立的终端,终端一多,标签页互相看不清,命令历史各自孤立,想要统一搜索过去的 AI 指令和反馈基本不可能。对于依赖 AI 做开发的团队来说,这些输入输出其实是很有价值的过程资产,丢掉了很可惜。

第三点是远程访问的问题。有时候我在开发机上跑着长任务,想用平板或另一台机器查看进度;原生 CLI 只绑定在当前终端里,完全没有这种能力。而 Web 化之后,只要服务监听在可访问的端口上,浏览器打开就能看。当然,远程访问必须做好鉴权,否则风险很大,这个后面我会专门说明。

基于这些需求,我的核心设计原则是:会话可重建、历史可查询、环境可切换、数据可备份。持久化不是给某个变量续一个命,而是把整个“AI 编码工作现场”完整地存储下来。

1.3 技术选型与实际组合

在技术选型上,我优先考虑的是“成熟稳定、社区资料多、自己熟”。最终确定的组合是:

  • 后端:Node.js + Express,负责 REST API 和静态资源服务。
  • 实时通道:WebSocket(ws库),负责浏览器终端与后端的双向数据流。
  • 终端模拟:node-pty在服务端创建伪终端,xterm.js在浏览器端渲染终端界面。
  • 持久化:默认用 SQLite(better-sqlite3),轻量且零配置;同时预留了 Redis 方案用于需要更高并发或多节点共享会话的场景。

为什么不直接用现成的服务器工具?因为现有的方案要么体积太大,要么不是专门为 AI CLI 优化。Node.js 生态里node-pty是服务器端伪终端最成熟的库,几乎所有的网页版终端项目都在用;搭配xterm.js,浏览器端渲染的体验可以做到非常接近原生终端。

为什么持久化层选 SQLite?因为我们的场景本质上是“单机多会话”,并发写不高,数据量也不大,SQLite 完全扛得住,还能避免引入额外的服务依赖。Redis 的真正优势是当工作区需要多实例部署、多台机器共用同一套会话状态时才体现出来。当然,如果你希望用 Redis 做实时数据通道或者更快的搜索,也可以在架构里加上它,只是要清楚地知道它承担的是什么角色。

2. 核心细节解析与实操要点

2.1 与 Claude Code 的集成方式

在 Easy Web Vibecoding 中集成 Claude Code 并不复杂,本质上就是通过node-pty启动一个真实的claude子进程,再把进程的输入输出重定向到 WebSocket 上。

关键点在于环境变量的传递。Claude Code 默认从环境变量读取模型配置和认证信息,比如:

  • ANTHROPIC_MODEL:指定使用的模型名。
  • ANTHROPIC_BASE_URL:指定 API 的 Base URL。如果你配置了兼容 Anthropic 协议的本地服务或第三方服务,可以在这里改指向。
  • ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN:认证凭证,需要从安全的环境变量文件注入,而不是硬编码进代码。

我遇到过一个高频坑:很多人在启动 Web 服务的时候,没有把当前用户 Shell 里的环境变量完整继承过来,结果claude命令能启动,但 AI 请求全部报错。后来我统一用一个prepareEnv()函数,把.env文件里的变量合并到process.env后再传给node-pty的spawn方法,这才稳定下来。

Claude Code 还提供了--continue参数,用于延续最近一次会话上下文。在持久化工作区里,我把这个能力封装成了一个“恢复会话”按钮:用户进入某个历史项目时,工作区会重新创建 PTY,并执行claude --continue(或指定--resume对应会话),同时前端把该会话的历史输出重新渲染出来,给人一种“现场还在”的感觉。如果你的版本命令名有差异,先跑一下claude --help确认。

2.2 与 Codex 的集成方式

Codex 的接入方式与 Claude Code 类似,但有一些细微差别。Codex CLI 支持交互式 Shell 模式,启动后可以连续对话;也支持单次执行模式,适合自动化脚本调用。由于 Easy Web Vibecoding 的核心是“像终端一样长期使用”,所以我主要使用交互式 Shell 模式。

环境变量方面,Codex 主要关注:

  • OPENAI_MODEL:指定模型名。
  • OPENAI_BASE_URL:指定兼容 OpenAI 协议的接口地址。如果你配置了本地模型或 DeepSeek 这类兼容服务,这个变量很重要。
  • 认证信息:通常来自codex登录后生成的凭证文件,或者通过环境变量注入。

因为 Codex 的会话恢复机制跟 Claude Code 不一样,我开始时也踩了坑。后来我看了codex --help,发现它支持恢复历史会话的参数(在部分版本中为resume,具体要看安装版本)。所以我的实现是:抽象出一个buildCommand(engine, options)函数,根据引擎类型拼接不同的启动参数。这样前端的“启动新会话”“恢复会话”按钮就能统一逻辑,后端各自适配。

还有一个细节:Codex 在执行任务时会在终端输出大量结构化日志,包括工具调用、文件变更、命令行输出等。持久化这些内容很有价值,我会在数据库里给每条输出打上时间戳和会话 ID,方便后面按时间轴回放。

2.3 持久化层设计:会话、终端输出与元数据

工作区的持久化我采用了“两张核心表”的简单模型:

  • sessions表:记录会话的基本信息,包括 ID、项目目录、引擎类型(claude/codex)、创建时间、最后活动时间、状态(running/stopped)、配置快照。
  • outputs表:记录终端输出的原始流,包含会话 ID、时间戳、数据类型(stdout/stderr)、数据内容。

为什么要单独存 output 而不是直接存一个大的文本文件?因为拆开后可以方便地做按时间查询、按类型过滤、统计每个会话的 output 量,还能在 Web 前端实现类似“跳转到某个时间点”的功能。SQLite 的查询能力虽然不如 PostgreSQL,但处理这种量级完全够用。

下面是建表 SQL 的简化示例:

CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, project_dir TEXT NOT NULL, engine TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running', config_json TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS outputs ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(id) );

在实际使用中,建议给outputs表的session_id加上索引,否则会话一多,恢复历史时查询会明显变慢。

如果你选择 Redis 来承担持久化职责,就要特别注意 Redis 自身的持久化机制。默认情况下 Redis 是纯内存数据库,重启后所有内存数据都可能丢失,除非开启 RDB 快照或 AOF 日志。我的建议是:如果你用它做会话存储,必须同时开启appendonly yes,并且配置appendfsync everysec作为性能和安全的折中。这样即使进程崩溃,也最多丢失 1 秒的数据。相比之下,SQLite 天然是文件型持久化,省心很多。

2.4 配置管理与密钥保护

由于工作区需要管理多个项目的密钥、模型参数和引擎配置,我制定了一套简单的配置规范:

  1. 所有敏感信息(API Key、Token)放在工作区根目录的.env文件里,使用dotenv加载。
  2. 不同项目的工作目录和默认模型放在config.json中,不涉及密钥。
  3. 每次启动新的 AI 会话时,后端只把该会话需要的环境变量子集传给node-pty,避免把全量环境变量暴露给子进程。

我见过有人图省事,把 API Key 直接写在启动命令里,结果通过ps能直接看到,非常危险。正确的做法是从环境变量里读取,例如:

const env = { ...process.env, ANTHROPIC_MODEL: 'claude-sonnet-4-20250514', ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || '' };

如果 Web 服务需要暴露到局域网或公网访问,我还会在上游加一层密码认证。最简单的方式是用 Nginx 基本认证,或者在 Express 里加一个前置中间件检查 Cookie。不要裸奔——因为你的终端里有真实的 API 密钥和项目代码,一旦被未授权访问,后果不是闹着玩的。

3. 实操过程与核心环节实现

3.1 环境准备与基础依赖

在开始之前,你需要准备以下环境:

  • Node.js 18 或更高版本。
  • Claude Code 命令行工具已经安装并登录。
  • Codex 命令行工具已经安装并登录(如果没有用到 Codex,可以先不装)。
  • 一个用来测试的项目目录,里面最好有一些代码文件。

我的 Windows 实测中发现,node-pty在 Windows 上需要编译原生模块,可能会卡在 build 阶段。建议直接使用 [Windows Terminal + 开发者模式] 环境,并且先安装windows-build-tools或者更新到较新的 Node.js 版本,让预编译二进制能直接被下载安装。Linux 和 macOS 一般比较顺利。

安装依赖只用到四个核心包:

npm init -y npm install express ws node-pty better-sqlite3 dotenv

前端需要使用xterm.js。我更习惯直接把xterm从 npm 包复制到public/vendor/xterm目录下,避免引入复杂的打包工具。生产环境下这样做简单粗暴但有效。

3.2 搭建 Web 服务与 WebSocket 通信

后端架构非常简单:Express 提供静态文件和 REST API,ws库处理终端数据的上行和下行。核心的服务端入口代码如下:

const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const path = require('path'); const { spawnWithSession } = require('./sessionManager'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); // API: 创建会话 app.post('/api/sessions', async (req, res) => { const { projectDir, engine, model, resume } = req.body; const session = await spawnWithSession({ projectDir, engine, model, resume }); res.json({ sessionId: session.id }); }); // WebSocket: 客户端连接时带上 sessionId 参数 wss.on('connection', (ws, req) => { const params = new URLSearchParams(req.url.split('?')[1]); const sessionId = params.get('sessionId'); if (!sessionId) { ws.close(); return; } // 注册客户端到对应会话 registerWebSocket(sessionId, ws); }); server.listen(3000, () => { console.log('Easy Web Vibecoding running at http://localhost:3000'); });

这里我没有把node-pty的细节放进 WebSocket 回调里,而是封装成sessionManager模块。这样 REST API、WebSocket、CLI 进程三者解耦,维护起来会舒服很多。

前端部分,我用xterm.js渲染终端,通过 WebSocket 连接到对应会话:

const term = new Terminal(); term.open(document.getElementById('terminal')); const ws = new WebSocket(`ws://${location.host}/?sessionId=xxx`); ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'output') { term.write(data.content); } }; term.onData((data) => { ws.send(JSON.stringify({ type: 'input', content: data })); });

实际使用中还应该加一个fit插件,让终端尺寸跟随浏览器窗口变化。窗口尺寸变化时,通过 WebSocket 告诉服务端调用pty.resize(columns, rows),否则终端排版会出现换行错乱。

3.3 接入 Claude Code CLI 的完整代码思路

sessionManager的核心逻辑是创建 PTY 并管理生命周期。下面是我最初实现时的核心代码,经过多轮重构后的思路非常清晰:

const os = require('os'); const pty = require('node-pty'); const crypto = require('crypto'); const { saveOutput, createSession, updateSessionStatus } = require('./store'); function spawnWithSession({ projectDir, engine, model, resume }) { const sessionId = crypto.randomUUID(); const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash'; const args = buildCliArgs(engine, { model, resume }); const env = { ...process.env, // 确保子进程的 cwd 是项目目录 PWD: projectDir, }; const ptyProcess = pty.spawn(shell, args, { name: 'xterm-256color', cols: 120, rows: 30, cwd: projectDir, env, encoding: 'utf8' }); createSession({ id: sessionId, projectDir, engine, status: 'running' }); ptyProcess.onData((data) => { // 推送给浏览器客户端 sendToClients(sessionId, { type: 'output', content: data }); // 写入持久化存储 saveOutput(sessionId, 'stdout', data); }); ptyProcess.onExit(({ exitCode }) => { updateSessionStatus(sessionId, 'stopped'); sendToClients(sessionId, { type: 'exit', code: exitCode }); }); return { id: sessionId, write: (data) => ptyProcess.write(data), kill: () => ptyProcess.kill(), resize: (cols, rows) => ptyProcess.resize(cols, rows) }; } function buildCliArgs(engine, { model, resume }) { if (engine === 'claude') { const args = ['-m', 'claude']; // 实际需要根据你的安装方式调整 if (resume) args.push('--continue'); if (model) args.push('--model', model); return args; } // Codex 的构建逻辑见下一节 }

有同学可能会问:pty.spawn的第一个参数不是必须可执行文件吗?为什么传bash而不是claude?其实node-pty支持像bash这样带参数的启动方式,它会先启动 Shell,然后让 Shell 运行claude。这样做的优势是能继承 Shell 的环境变量、加载用户自己的 PATH 配置,尤其是用 nvm 或 fnm 管理 Node 版本的环境,直接 spawnclaude反而容易找不到命令。

当然,如果你确定claude的全局路径很稳定,也可以直接 spawn 绝对路径,比如/usr/local/bin/claude。但用 Shell 启动的通用性更强,也更符合用户实际终端习惯。

3.4 接入 Codex CLI 的差异化处理

Codex CLI 的启动命令和交互方式与 Claude Code 略有差异,所以我单独实现了buildCliArgs的 Codex 分支:

if (engine === 'codex') { const args = ['-c', 'codex']; // 同样先进入 shell 再执行 codex if (resume) args.push('resume'); // 具体参数以你的 codex --help 为准 return args; }

如果你使用的 Codex 版本没有resume命令,也可以退而求其次:在工作区界面上提供一个“加载历史上下文”按钮,其实现方式是把之前的会话内容作为一段文字放入新会话的输入框,让 AI 重新“读一遍”历史。虽然这和原生上下文延续不完全一样,但在很多实际场景中已经足够使用。

Codex 在启动时会检测是否已经登录授权。如果检测到未登录,它会在终端里输出一个 URL 或二维码让你完成授权。在 Web 工作区里,这个过程是透明的:前端直接把输出渲染出来,你可以点击输出中的链接完成授权。完成后继续在终端里输入指令即可。

这里要额外提醒一下:Codex 的这个交互过程可能会等待输入,比如“Press Enter to continue”。如果你把输入流直接透传了,问题是浏览器端的用户不一定知道现在需要输入。我的做法是在输出层做一个简单的关键字检测,当输出包含[Press Enter]之类的提示时,在前端终端上方显示一条“AI 正在等待键盘输入”的状态栏。

3.5 持久化实现:SQLite 核心代码思路

持久化层我用better-sqlite3封装了一个store.js,全部使用同步 API。有人可能会问“同步 API 不是会卡线程吗?”实际上在 Node.js 单线程模型下,SQLite 的同步调用如果写得很频繁,确实会阻塞事件循环。但在我们的场景中,终端的 output 频率并不高,且每次写入都是几 KB 级别的插入操作,实测完全无感。如果未来要支持超高并发,再改成异步批量写入也不迟。

核心代码如下:

const Database = require('better-sqlite3'); const db = new Database('vibecoding.db'); db.exec(` CREATE TABLE IF NOT EXISTS sessions (...); CREATE TABLE IF NOT EXISTS outputs (...); `); function createSession(sessionInfo) { const stmt = db.prepare( 'INSERT INTO sessions (id, project_dir, engine, status) VALUES (?, ?, ?, ?)' ); stmt.run(sessionInfo.id, sessionInfo.projectDir, sessionInfo.engine, 'running'); } function saveOutput(sessionId, type, content) { const stmt = db.prepare( 'INSERT INTO outputs (session_id, type, content) VALUES (?, ?, ?)' ); stmt.run(sessionId, type, content); } function getHistory(sessionId) { const stmt = db.prepare( 'SELECT content FROM outputs WHERE session_id = ? ORDER BY id' ); return stmt.all(sessionId).map((row) => row.content).join(''); } module.exports = { createSession, saveOutput, getHistory, ... };

恢复会话时,前端会先调用/api/sessions/:id/history获取历史输出并渲染到终端,然后发送一个“启动恢复”的 WebSocket 消息,后端再重新创建 PTY 并运行claude --continue(或对应的恢复命令)。这样一来,界面上既有之前的历史,又是全新的交互进程,体验很接近“工作区恢复”。

3.6 可选:Redis 持久化与重启恢复

如果你的环境里本来就有 Redis,也可以让工作区在启动时优先连接 Redis,把会话元数据和输出流写入 Redis,同时开启 AOF。宕机后 Redis 数据不会丢,工作区也能在启动后自动从 Redis 恢复未归档的会话。

Redis 的数据结构很直接:

  • 用HASH存会话元数据:HSET session:{id} project_dir ... engine ...
  • 用LIST存每个会话的输出流:RPUSH output:{id} <content>
  • 用SET存所有活跃会话 ID:SADD active_sessions {id}

这样设计的好处是获取某个会话的所有输出只需要LRANGE output:{id} 0 -1,非常快。而且 Redis 本身支持键过期,你可以设置一个 TTL 让超过 30 天的归档输出自动清理,避免无限膨胀。

我最终的生产环境是“SQLite + Redis 双写”的冗余方案:SQLite 作为持久化主存储,Redis 承担实时推送和临时状态存储。如果你只想要简单可靠,SQLite 一个就够。

4. 常见问题与排查技巧实录

4.1 CLI 命令找不到或退出码异常

症状:点击“启动会话”后,终端一片空白,刷新之后看到会话状态变成 stopped,日志里有类似spawn ... ENOENT的错误。

原因通常是两种:一是服务端的 PATH 没有包含claude或codex的安装路径;二是通过 Shell 启动时,Shell 配置文件没有被加载。

解决思路:先在启动 Web 服务前手动在同一个终端里执行which claude或where claude,确认命令路径。然后在sessionManager里,把PATH字段明确加上命令目录,例如:

env.PATH = `/usr/local/bin:${env.PATH}`;

另外,如果你用 nvm 管理 Node 版本,claude可能会装到某个特定版本的 Node 目录下,直接注入完整路径更稳。建议把 CLI 命令路径做成可配置项,放在工作区的config.json里。

4.2 认证失效与 token 问题

故障表现:Claude Code 或 Codex 启动后,输出报错提示认证失效,或者出现类似auth token is unavailable的字样。

大多数情况下,原因是 Web 服务进程没有继承你登录 CLI 时生成的认证信息。CLI 登录后通常会生成一个脱机的凭据文件,比如~/.claude/.credentials或~/.codex/auth.json。当你从桌面快捷方式、systemd 服务或其他工具启动 Web 服务时,HOME 目录可能指向了错误的位置,导致 CLI 找不到凭据。

排查方法:在 Web 服务启动前的登录会话里运行一次echo $HOME,并确认该目录下的凭据文件存在。如果 Web 服务是常驻进程,可以写一个小接口/api/health返回当前进程的HOME和PATH,方便远程诊断。

如果认证实在无法恢复,就在 Web 工作区里进入会话,手动执行claude/codex的登录流程,重新完成一次授权。授权结束后不要关闭终端,直接在同一条会话里继续工作即可。

4.3 并发会话隔离

同时开多个项目会话时,最容易出现的两个问题:一是多个 PTY 共享同一个工作目录,导致文件冲突;二是多个会话同时调用同一个 API Key,触发速率限制。

我在设计会话模型时强制要求:每个会话绑定一个唯一的projectDir,同一时间同一目录只能有一个活动会话。如果用户尝试在同一个目录开启第二个会话,后端会返回冲突提示,并建议先停止前一个会话。

实现一个简单的内存锁即可:

const activeDirectories = new Map(); function canStartSession(projectDir) { if (activeDirectories.has(projectDir)) return false; activeDirectories.set(projectDir, true); return true; }

会话退出或删除时再释放锁。这种方式虽然不能避免两个不同会话同时操作同一个目录(比如通过不同的符号链接路径),但已经能覆盖绝大多数使用场景。

4.4 本地模型接入:LMStudio / DeepSeek 兼容 API

很多人希望让 Claude Code 或 Codex 接上本地模型,比如 LMStudio 启动的本地模型,或者 DeepSeek 这类兼容 API 的服务。这在 Easy Web Vibecoding 里也很容易实现,核心是 Base URL 配置。

以 Claude Code 为例,如果你用 LMStudio 提供兼容 Anthropic 协议的接口,可以在.env里设置:

ANTHROPIC_BASE_URL=http://127.0.0.1:1234/v1 ANTHROPIC_MODEL=local-model-name

然后启动工作区时,这个 Base URL 会被传递给 PTY 子进程。注意 LMStudio 或本地推理服务必须已经启动,并且能接受外部连接。如果 Base URL 没配置对,常见的报错是“连接拒绝”或“模型不存在”。

Codex 同理,DeepSeek 之类的服务通常提供 OpenAI 兼容接口,只需要设置:

OPENAI_BASE_URL=https://api.deepseek.com/v1 # 仅示例,请以实际服务商地址为准 OPENAI_MODEL=deepseek-chat

这样在工作区里启动 Codex,它就会通过该 Base URL 发起请求。我建议你为不同的模型服务准备多套.env文件,比如.env.claude和.env.codex,在工作区界面增加一个模型配置文件下拉菜单,每次启动会话时动态选择要加载的环境变量。这样可以在同一个界面里无缝切换官方模型、本地模型和第三方兼容模型。

4.5 输出丢失和 WebSocket 断线恢复

前端浏览器刷新或者网络中断后,PTY 进程还活着,但 WebSocket 连接已经断开。如果后端直接把数据推到断开的客户端,数据就丢了。

我的解决方案是“客户端重连后补齐增量”:在 WebSocket 层维护每个会话的lastSequenceId,前端每次重连时把当前已看到的最大输出序号传给后端;后端查询数据库,把该序号之后的输出全部补发给前端。因为所有输出都在 SQLite 里,这个操作实现起来很轻松。

实际测试中,即使前端断线 10 分钟再重连,只要 PTY 进程还活着,终端内容也能完整补回来。这个体验比原生 CLI 强太多——原生终端滚动缓冲区一断,内容就没了。

4.6 Redis 持久化数据不重生的排查

如果你使用了 Redis 作为存储,重启后发现会话数据全空了,请检查 Redis 是否开启了 AOF。运行redis-cli config get appendonly,如果返回no,数据必然丢。开启方式:

redis-cli config set appendonly yes redis-cli config rewrite

然后再测试重启,数据就能恢复。另外,Redis 的 RDB 快照默认在后台保存,但快照间隔可能较长,最好是 AOF 和 RDB 同时开启,AOF 作为主要恢复源。

如果你发现 restarted 后数据有一部分恢复了但缺少最后几秒的输出,那是因为appendfsync设置为everysec,崩溃时恰好丢失了聚合窗口内的写操作。在非关键场景下这是可接受的;如果要更强保证,可以改为always,但会显著降低写性能。

5. 扩展方向与个人经验

5.1 加一个快捷键面板:快速切换项目

工作区用顺手之后,我加了一个非常受欢迎的功能:快捷键面板。按下Ctrl+K会弹出一个命令面板,可以输入项目名模糊搜索、切换当前会话、快速打开历史输出列表或切换引擎。

实现不复杂:在 Express 里加一个/api/sessions的列表接口,返回所有会话;前端做一个简单的搜索框,点击后通过 WebSocket 发送切换消息。这个功能让多项目协作的体验提升了一个台阶,强烈建议你也试试。

5.2 用 VS Code 还是浏览器?我的取舍

很多人问我:为什么不直接在 VS Code 终端里用 Claude Code?我的回答是:VS Code 终端确实更轻,但它做不到“会话持续挂在后台且多端可见”。我的工作区初衷是让 AI 编码会话独立于 IDE 存在,Browser 端只是查看器,真正干活的地方可以是任何一台装了 CLI 的服务器。

比如我经常在开发机上跑长时间 AI 任务,然后人转到另一台电脑上打开浏览器查看进度,必要时远程输入命令。原生终端绑定在当前设备上,做不到这一点。如果你只在一台机器上单项目使用,VS Code 终端完全够用;但如果你和我一样有多项目并行、远程访问、历史回溯的需求,Web 工作区是不可替代的。

5.3 长期使用的心得与备份建议

从开始搭建到现在,我几乎把日常的 AI 编码工作全都搬进了这个工作区。用下来的体会是:工具本身不难,难的是养成“提交配置、归一会话、备份数据”的习惯。我给每个项目建立独立的配置文件,每次启动新会话前先确认当前环境变量是否指向正确的目录和模型;每天结束前,把 SQLite 数据库文件拷贝到备份目录或网盘。

另外一个小心得:给会话命名。我在 sessions 表里加了一个name字段,启动时可以通过 API 传入,比如“项目A-重构解析器”、“项目B-修登录bug”。这样半个月后回看列表,依然能一眼找到当时的上下文,比只显示时间和目录清楚得多。

如果你也想给自己的 AI 编码流程加一个“持久层”,完全可以按照这篇文章的思路自己搭一套 Easy Web Vibecoding。不要被“工程化”三个字吓到,核心代码其实就几百行,最关键的只是 PTY 管理和数据库存储。刚开始可以用最小版本跑通,再逐步加功能。等你真的用起来,就会明白“随时可恢复的 AI 工作现场”是一种多么安心的体验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询