
1. 这个项目到底在折腾什么把 Claude Code 塞进手机听起来像是个极客的玩具项目但我实际动手的起因很朴素每天通勤来回两个多小时地铁上经常突然想到某个脚本要改、某段配置要调掏出笔记本不现实手机倒是一直在手里。Claude Code 本身是个跑在终端里的命令行工具官方支持 macOS、Linux 和 Windows手机端并没有现成的方案。我最初的想法很简单能不能让手机通过某种方式连上跑在别处的 Claude Code 实例把手机当成一个瘦客户端来用。这个思路其实和早年用手机 SSH 连服务器写代码是一个逻辑。手机负责输入输出真正的计算和文件操作在远端完成。但 Claude Code 和普通编辑器不一样的地方在于它是个交互式的 AI 编程助手会持续读写文件、执行命令、维护会话上下文对终端环境的要求比 vim 高不少。我试过直接用手机上的终端模拟器连过去能用但体验很割裂键盘遮挡、复制粘贴别扭、会话断了要重连用了几次就放弃了。后来我换了个方向既然 Claude Code 的核心是一个 CLI 进程那我能不能把它包装成一个可以被手机浏览器访问的服务手机端只需要一个网页界面后端跑一个常驻的 Claude Code 会话通过 WebSocket 把输入输出双向转发。这样手机端不依赖任何特殊 App浏览器打开就能用换设备也不影响。这个方案跑通之后我把整套东西整理成了一个开源项目也就是标题里说的“开源了”。需要先说明一点这个项目解决的不是“在手机上本地运行 Claude Code”这个问题。手机处理器再强跑一个完整的 Node.js 运行时加 Claude Code 的依赖链也不现实而且 Claude Code 本身需要调用远端模型接口本地算力并不是瓶颈。真正有价值的是把交互层和计算层解耦让手机成为一个随时可用的入口。适合参考这个项目的人包括经常需要在移动场景下处理代码的开发者、想给自己的 CLI 工具做移动端封装的工程师、以及对终端复用和 WebSocket 转发感兴趣的人。哪怕你用的不是 Claude Code这套架构换成任何交互式 CLI 工具都能套用。2. 整体架构设计与技术选型2.1 为什么不做原生 App 而选 Web 方案最开始我认真考虑过写一个原生 Android App用 Termux 或者自己嵌一个终端控件。放弃的原因有三个。第一原生 App 要处理 Android 和 iOS 两套代码维护成本直接翻倍而我一个人做这个项目精力有限。第二原生终端控件在移动端的输入体验很难做好尤其是组合键、方向键、Tab 补全这些终端高频操作触屏上做映射很别扭。第三App 要上架、要签名、要处理各种权限迭代一次周期太长而 Web 方案改完刷新就能看到效果。Web 方案的核心优势是零安装成本。手机浏览器打开一个地址就能用不需要装任何东西也不受应用商店审核限制。对于自部署的工具来说这一点太重要了。你可以在自己的服务器或者家里的电脑上跑服务手机随时随地访问数据和控制权都在自己手里。当然 Web 方案也有代价。浏览器对终端的支持不如原生控件尤其是需要精确控制光标位置和字符渲染的场景。我最后选的是 xterm.js 这个库它是 VS Code 终端用的同一套渲染引擎兼容性和性能都经过大规模验证。xterm.js 负责前端的终端渲染和键盘事件捕获后端用 node-pty 创建伪终端进程来跑 Claude Code两端通过 WebSocket 传输数据。这个组合是目前 Web 终端方案里最成熟的VS Code Server、code-server 这些项目都是类似思路。2.2 通信层为什么用 WebSocket 而不是 HTTP 轮询终端交互的本质是持续的、双向的、低延迟的数据流。你敲一个字符终端要立刻回显Claude Code 输出一段内容要实时推到手机屏幕上。HTTP 轮询在这种场景下完全不可行每次请求都要重新建立连接、带一堆头部信息延迟高得没法用。WebSocket 建立一次连接之后就是全双工通道两端可以随时互发数据延迟基本就是网络往返时间。对于终端这种场景WebSocket 是唯一合理的选择。我在实现的时候还加了一层心跳机制每隔 30 秒发一个 ping防止中间的负载均衡或者网络设备把空闲连接掐掉。这个细节很关键我一开始没加心跳结果发现手机锁屏几分钟后连接就断了排查了半天才定位到是连接被中间层回收了。数据传输格式上我直接用二进制帧传原始字节流不做额外的 JSON 包装。终端数据本身就是字节流中间加一层序列化反序列化只会增加延迟和复杂度。前端 xterm.js 的 onData 事件拿到用户输入直接通过 WebSocket 发出去后端 node-pty 的输出直接转发到前端整条链路非常干净。2.3 会话管理断线重连与多设备接入移动场景下网络切换是常态地铁进隧道、WiFi 切 4G连接断掉是必然的。如果每次断线都要重新启动 Claude Code 进程那之前积累的会话上下文就全丢了体验极差。所以我在后端做了一个会话保持层Claude Code 进程独立于 WebSocket 连接存在连接断开时进程继续跑只是把输出缓冲起来重新连接时前端先请求一份缓冲区里的历史输出把屏幕内容恢复出来然后继续实时接收。这个设计参考了 tmux 的思路。tmux 之所以在远程开发里这么流行核心就是它把会话和连接解耦了你断线重连之后看到的东西和断线前一模一样。我在 Web 层复现了这个行为用一个环形缓冲区存最近的输出默认保留 100KB 左右足够覆盖大部分场景的屏幕恢复需求。多设备接入方面我允许同一个会话被多个 WebSocket 连接同时订阅。这样你可以手机和平板同时开着两边看到的内容实时同步。实现上就是维护一个订阅者列表node-pty 有输出时遍历列表逐个推送。需要注意的是输入只能有一个来源否则两个设备同时打字会乱套所以我在协议里区分了“可写连接”和“只读连接”同一时间只允许一个可写连接。3. 核心实现细节与关键代码3.1 后端伪终端进程的创建node-pty 是这套方案的核心依赖它能在 Node.js 里创建一个真正的伪终端让 Claude Code 以为自己跑在一个正常的终端环境里。这一点很重要很多 CLI 工具会检测自己是否运行在 TTY 环境如果不是就关闭颜色输出或者改变交互行为。用 child_process 直接 spawn 是不行的必须用 node-pty 才能拿到完整的终端语义。创建伪终端的代码大致是这样const pty require(node-pty); const term pty.spawn(claude, [], { name: xterm-256color, cols: 120, rows: 30, cwd: process.env.HOME, env: { ...process.env, TERM: xterm-256color } }); term.onData((data) { // 把输出推送给所有订阅的 WebSocket 连接 broadcast(data); });这里有几个参数需要说明。name设为xterm-256color是为了让 Claude Code 输出 256 色终端界面会好看很多。cols和rows是初始终端尺寸前端连接建立后会用实际屏幕尺寸覆盖这两个值。cwd设成用户主目录你也可以改成某个固定的项目目录这样每次启动都直接进到工作区。注意node-pty 是原生模块安装时需要编译对系统的构建工具链有要求。在 Linux 上需要 python3、make、g在 macOS 上需要 Xcode Command Line Tools。如果你在 Docker 里跑基础镜像要选带这些工具的或者用多阶段构建。3.2 终端尺寸同步的处理移动端屏幕尺寸千差万别横竖屏切换、软键盘弹出都会改变可用空间。终端尺寸如果不同步会出现换行错乱、光标位置不对的问题。前端 xterm.js 提供了 fit 插件可以根据容器大小自动计算合适的 cols 和 rows然后通过 WebSocket 把新尺寸发给后端后端调用term.resize(cols, rows)通知伪终端。这里有个坑我踩过软键盘弹出时浏览器视口高度会变化如果直接监听 resize 事件去 fit会在键盘动画过程中频繁触发导致终端反复重排看起来一直在闪。我的处理是加一个防抖resize 事件停止 200 毫秒后才执行 fit 和尺寸同步。另外在移动端要用visualViewport的 resize 事件而不是 window 的前者能更准确地反映键盘弹出后的实际可视区域。3.3 前端键盘输入的特殊处理手机上的软键盘和物理键盘差别很大很多终端需要的按键在软键盘上没有。比如 Esc、Tab、Ctrl 组合键、方向键这些在 Claude Code 里都是高频操作。我的方案是在终端界面上方加一排快捷键按钮点击时直接往 WebSocket 发送对应的控制字符。控制字符的映射关系需要记清楚CtrlC 是\x03CtrlD 是\x04Esc 是\x1bTab 是\x09上箭头是\x1b[A下箭头是\x1b[B。这些是终端领域的标准约定任何终端模拟器都遵循。我把常用的一组做成了按钮实际用下来覆盖率能到九成以上。还有一个细节是输入法的问题。中文输入法在 xterm.js 里直接打字会有问题因为输入法需要先经过候选词阶段而 xterm.js 默认把每个按键都当成终端输入。解决办法是监听 compositionstart 和 compositionend 事件在输入法组合期间不往终端发数据组合结束后再一次性发送最终文本。这个处理不做的话中文基本没法输入。3.4 会话持久化的存储策略前面提到用环形缓冲区保存输出但缓冲区在内存里服务重启就没了。如果你希望服务重启后还能恢复会话需要把输出落盘。我的做法是每个会话对应一个日志文件输出同时写入缓冲区和文件重连时优先从内存缓冲区恢复缓冲区不够时从文件读取。文件不能无限增长我设了一个上限比如单文件 10MB超过就滚动保留最近的两个文件。这个策略和日志轮转是一个思路。实际用下来一个活跃会话一天产生的输出也就几百 KB10MB 够用很久。提示如果你在多人共用的服务器上部署会话文件要注意权限设置避免不同用户的会话数据互相可见。我默认把会话文件放在用户主目录下的隐藏目录里权限设成 600。4. 部署与实操全流程4.1 环境准备与依赖安装整套服务跑起来需要 Node.js 18 以上版本推荐用 20 LTS。先确认本机 Node 版本node -v npm -v如果版本不够用 nvm 装一个curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash nvm install 20 nvm use 20然后拉取项目代码安装依赖git clone 项目地址 cd 项目目录 npm installnpm install 这一步会编译 node-pty如果报错大概率是缺构建工具。Ubuntu 上装build-essentialmacOS 上装 Xcode Command Line ToolsWindows 上需要 Visual Studio Build Tools 里的 C 工作负载。Claude Code 本身要提前装好并且完成登录配置确认在普通终端里能正常跑起来。这一步不做的话伪终端里启动会直接失败。4.2 服务配置与启动项目根目录下有个配置文件主要配这几项监听端口、访问令牌、默认工作目录、会话缓冲区大小。访问令牌是必须设的因为服务暴露出来之后任何知道地址的人都能操作你的终端这个风险太大。令牌用随机字符串越长越好。# 生成一个随机令牌 openssl rand -hex 32把生成的字符串填到配置里启动服务npm start看到监听日志输出就说明起来了。本地先测一下浏览器打开http://localhost:端口输入令牌应该能看到终端界面并且 Claude Code 正常启动。4.3 手机端访问的网络配置手机要访问这个服务得保证手机和服务器在同一网络或者服务器有公网地址。同一 WiFi 下最简单查一下服务器的局域网 IP手机浏览器直接访问http://局域网IP:端口就行。如果要在外网访问方案就多了。可以用反向代理把服务暴露到公网配好 HTTPS这样手机在任何网络下都能连。反向代理我推荐用 Caddy配置简单自动申请证书your-domain.com { reverse_proxy localhost:端口 }WebSocket 的转发 Caddy 会自动处理不用额外配置。用 Nginx 的话需要手动加 Upgrade 和 Connection 头容易漏。注意暴露到公网一定要用 HTTPS并且令牌要足够强。明文 HTTP 下令牌会在网络上裸奔被截获就是终端被完全控制。另外建议在反向代理层加一层访问限制比如只允许特定地区 IP 访问进一步降低风险。4.4 手机端使用体验优化服务跑起来之后手机端还有几个体验优化值得做。第一是加到主屏幕用浏览器的“添加到主屏幕”功能这样打开就像个 App没有地址栏占空间。第二是配置 manifest 和图标让全屏显示更自然。第三是禁用页面的缩放和滚动终端区域要固定住不然手指滑动会带着整个页面动。xterm.js 的配置里把scrollback设大一点比如 5000 行方便往上翻看历史输出。字体大小在手机上建议 12 到 14太小看不清太大一屏显示不了几行。行高设 1.2 左右比较舒服。实际用下来横屏体验比竖屏好很多因为终端本来就是宽大于高的布局。我在界面上加了个横屏提示检测到竖屏时建议用户旋转。当然这个不是强制的竖屏也能用只是每行显示的字符少一些。5. 常见问题与排查实录5.1 连接建立失败最常见的问题是 WebSocket 连不上。排查顺序是这样先确认服务本身在跑用curl http://localhost:端口看有没有响应再确认反向代理配置WebSocket 需要特殊的头转发漏配的话握手会失败最后看浏览器控制台的报错如果是 401 就是令牌不对如果是 502 就是代理到后端的连接有问题。还有一个隐蔽的坑是某些网络环境会拦截 WebSocket 连接。这种情况在浏览器控制台会看到连接直接失败没有 HTTP 状态码。解决办法是让 WebSocket 走标准 HTTPS 端口伪装成普通 HTTPS 流量中间设备就不容易识别和拦截了。5.2 终端显示乱码或错位乱码通常有两个原因。一是字符编码不一致前端和后端都要用 UTF-8。二是终端类型不匹配node-pty 创建时设的 TERM 要和前端 xterm.js 声明的一致都用xterm-256color。显示错位一般是尺寸不同步导致的。检查前端有没有正确调用 fit 并把尺寸发给后端后端有没有调用 resize。如果尺寸对但内容还是错位可能是缓冲区恢复时没有正确处理控制序列导致光标位置计算错误。这种情况可以在恢复前先发一个清屏序列\x1b[2J\x1b[H把屏幕重置干净再回放历史。5.3 Claude Code 启动异常如果伪终端里 Claude Code 起不来先在普通终端里确认它能正常运行。常见原因是环境变量缺失比如 PATH 里找不到 claude 命令或者认证相关的配置没有正确传递。node-pty 创建进程时可以显式传 env把必要的环境变量带上。另一个原因是工作目录权限问题。如果 cwd 设成了一个当前用户没有写权限的目录Claude Code 启动时读写文件会失败。确认一下目录权限或者换一个用户有完全控制权的目录。5.4 会话断线后无法恢复断线恢复依赖缓冲区里的历史输出。如果恢复后屏幕是空的或者内容不全检查缓冲区大小是不是设得太小或者会话文件有没有正确写入。还有一种情况是服务重启了内存缓冲区清空这时候要从文件恢复确认文件路径配置正确并且文件确实存在。如果恢复出来的内容有重复那是缓冲区边界处理的问题。环形缓冲区在回绕的时候如果索引计算不对会把旧数据和新数据混在一起。这个 bug 我在早期版本遇到过后来改成用固定大小的数组加写指针逻辑就清晰了。问题现象可能原因排查方向WebSocket 连不上代理未转发 Upgrade 头检查反向代理配置终端乱码编码或 TERM 不一致统一 UTF-8 和 xterm-256color显示错位尺寸未同步检查 fit 和 resize 调用Claude Code 起不来环境变量或权限问题普通终端先验证断线无法恢复缓冲区或文件配置问题检查缓冲区大小和文件路径中文输入异常输入法组合事件未处理监听 composition 事件6. 这套方案还能怎么扩展跑通基础版本之后我陆续加了一些扩展功能这里挑几个实用的说说。第一个是会话列表后端维护多个 Claude Code 会话前端可以切换。这样你可以同时开几个不同项目的会话互不干扰。实现上就是把单个 pty 进程的管理扩展成进程池每个会话有独立的 ID 和缓冲区。第二个是输出搜索。终端里翻历史很痛苦尤其是输出很长的时候。我在前端加了个搜索框输入关键词后高亮匹配的行并且可以跳转。xterm.js 提供了搜索插件直接集成就行不用自己实现。第三个是文件上传。手机上想给 Claude Code 喂一个文件靠终端里敲路径不现实。我加了个上传入口文件传到服务器后把路径插入到终端输入里Claude Code 就能直接读取。这个功能在处理日志、配置文件的时候特别方便。再往远了想这套架构其实可以泛化成通用的“移动端 CLI 网关”。任何交互式命令行工具只要把它跑在伪终端里前面套上这套 WebSocket 转发和会话管理就能获得移动端访问能力。数据库客户端、远程调试工具、系统监控工具都能这么搞。核心思路就一句话把交互层和计算层解耦让终端会话独立于连接存在。这个思路我在实际项目里反复用到每次都能省下大量重复工作。最后分享一个我在调试这套东西时总结的小技巧遇到终端行为异常先用script命令在本地录一段正常的终端会话把原始字节流存下来然后拿这段数据去对比你的转发链路输出。差异点往往就是问题所在。这个方法比盯着代码猜快得多尤其是处理控制序列相关的问题时。