自建 ClawChat:用 frp 把内网 AI 助手搬到手机上
折腾 OpenClaw 有一段时间了。这个框架挺好用的,跑在笔记本上当私人 AI 助手,支持接 Telegram、Discord、WhatsApp 等各种 channel。问题是,这些在国内全被墙了。微信倒是没被墙,但它没有开放 bot API——这是另一种意义上的墙。
所以我一直只能在本机的 web UI 上用,出门就断联。
最近花了两天,自己写了一个叫 ClawChat 的轻量 PWA 页面,配合 frp 内网穿透,现在可以在任何地方用手机访问内网里的 OpenClaw 了。整个方案不依赖任何第三方 IM,体验比我预期的好很多。
架构长这样
手机浏览器 / PWA (chat.wenb.in)
↓ HTTPS
公网服务器 (Azure Japan East,Caddy 反代)
↓ frp 隧道
内网笔记本 → OpenClaw Gateway :18789没有公网 IP,所以需要 frp 打洞。公网服务器跑 Caddy 负责 HTTPS 和 WebSocket 反代,frp 把内网的 gateway 端口透出去。前端是一个纯静态 HTML 文件,直接部署在公网服务器上,通过 WebSocket 和 gateway 通信。
frp + Caddy 的配置
frp 这块没什么特别的,标准 TCP 转发:内网的 frpc 把 127.0.0.1:18789 转发到公网服务器的某个端口,然后 Caddy 反代过去。
# frpc.toml 核心部分
[[proxies]]
name = "openclaw-gateway"
type = "tcp"
localIP = "127.0.0.1"
localPort = 18789
remotePort = 18789frpc 跑成 systemd service,开机自启,不用操心。
Caddy 这边处理 HTTPS 和 WebSocket 升级,顺便把真实 IP 传给 gateway:
chat.wenb.in {
reverse_proxy /ws* localhost:18789
root * /var/www/clawchat
file_server
}gateway 那边要配 trustedProxies,不然拿到的全是 127.0.0.1。
WebSocket 协议:这里踩了最多坑
OpenClaw 的 gateway 有两个接入方式:OpenAI 兼容的 HTTP endpoint,和 Control UI 用的 WebSocket 协议。我最开始想用 HTTP 的,更简单嘛。但有个根本问题——HTTP 每次请求都是新 session,没有连续上下文。所以必须走 WebSocket。
WebSocket 协议的握手流程大概是这样:
- 连接建立后 gateway 发来
challenge - 客户端用 token 做认证,发回
connect消息 - 认证成功后才能发
chat.send - 收消息用
chat.history拉取
有几个细节文档里没写清楚,全靠看源码和抓包摸索出来:
delta 是累积全文,不是增量。 每次 delta 事件里的内容是从头开始的完整文本,不是新增的部分。我一开始叠加显示,结果消息变成了指数级增长的重复。
final 事件没有内容。 模型回复完成后发的 final 事件里消息体是空的,需要在收到 final 之后主动调一次 chat.history 才能拿到完整回复。
session key 的格式有讲究。 默认 agent 用 main,其他 agent 要用 agent:<id>:main。这个搞错的后果是所有消息都被路由到默认 agent,而且不报错——调了好一会才发现是这个问题。
PWA 单文件
整个 ClawChat 是一个 HTML 文件,没有任何构建工具,没有 npm,没有 node_modules。能支持:
- iOS 添加到主屏幕后全屏运行
- 深色模式(跟随系统)
- 多 Agent 切换
- 图片上传(直接传原始 base64,不经过 canvas 压缩——压缩会破坏截图里的文字)
历史消息要过滤一下。gateway 会把 toolCall 和 toolResult 全部返回,在聊天界面里显示没有意义,只保留 user 消息和每轮最后一条有文字内容的 assistant 回复。
iOS PWA 的两个坑
输入框不贴底。 用了 viewport-fit=cover 让页面延伸到刘海和底部安全区,但键盘弹出时输入框位置算得不对,飘在空中。最后用 visualViewport API 监听 resize 事件,动态计算输入框的 bottom 偏移,算是绕过去了。
manifest 改了要重装。 PWA 的 manifest 是安装时被缓存的,之后怎么改服务端的文件都没用,必须先从主屏幕删掉再重新添加。这个不知道坑了多少人。
用下来的感受
现在的日常是:笔记本开着跑 OpenClaw,手机打开 chat.wenb.in,就像用普通的聊天 app 一样。响应速度取决于模型本身,WebSocket 的延迟基本感知不到。
PWA 安装到主屏幕后体验确实接近原生 app——全屏、有图标、没有浏览器地址栏。对于个人使用来说够用了。
当然有明显的局限:笔记本要是关机或者断网,整个链路就断了。而且只有一个 token,本质上是单用户方案。但我本来就是给自己用的,这些都不是问题。
搭下来整体比想象中顺利,主要时间花在搞清楚 WebSocket 协议细节和处理 iOS 的各种奇怪行为上。代码量不大,但这种"完全控制"的感觉挺好的——没有第三方 IM 的账号限制,没有 API 配额,没有隐私顾虑,就是一条从手机到自己机器的直连隧道。