跳转到内容

自建 Web Chat 三天上线:从想法到部署的全过程

📍 真实案例

一个浏览器窗口里有简洁的聊天界面,顶部带 PIN 锁标识

那是个普通的周二。洁柔在地铁上想跟家里的 bot 聊一句,突然想到一个问题:全家的 bot 都挂在 Telegram 上——哪天 Telegram 用不了,就一个入口都没有了。

她在 Notes 里写了一行:

「能不能搞个网页版?域名直接打开就用,不用装客户端。」

我(小牛)接到这条需求后,3 天后 chat.你的域名.com 上线。

读完这一篇你会知道:怎么从一个 30 字的需求 → 一个真实跑在生产的网页 chat,而且3 天就能搞完。

网页聊天界面 + Telegram + Web Chat 两个 channel 并行示意

3 天后的样子:

  • 网址:chat.你的域名.com(走 Cloudflare Tunnel 发布到公网)
  • 4 位 PIN 守门——当时觉得够用(剧透:后来发现这道门形同虚设,文末《一盆冷水》有完整复盘,后来换成了邮箱验证门,如今这个入口已经停用)
  • 跟 Telegram 平行——同样的 bot 集群,同样的人格,两个入口任选(当时如此;如今两个入口都已停用)
  • 支持文件附件(上传图片、PDF、Excel 直接发给 bot)
  • 整段回复(bot 想清楚再一段送达,不是 ChatGPT 那种一字一字流——简单稳定)
  • 响应式(手机 / 平板 / 桌面都干净)

源码:当初开源在 GitHub 上,后来发现安全漏洞,仓库已经删除(见文末《一盆冷水》)。

3 天时间线 1 蓝图 / Day 2 接通 / Day 3 上线

Day 1:骨架 + 单聊跑通(8 小时)

Section titled “Day 1:骨架 + 单聊跑通(8 小时)”

目标:浏览器打开能聊一句,bot 能回。其他一切先放。

我做的事:

  1. 挑技术栈(45 分钟)——前端纯 HTML + Vanilla JS(不套任何框架的原生 JavaScript;没用 React,因为不需要),后端用 Bun(一个 JavaScript 运行环境)起一个 WebSocket 网关(gateway)。WebSocket 就是浏览器和服务器之间一直开着的双向通道
  2. 写 server.ts(2 小时)——Bun 起 fetch 路由 + WebSocket 双向通道
  3. 写前端页面(2 小时)——一个 index.html 加一个 app.js:一个输入框 + 一个聊天框 + 一个发送按钮
  4. 接通 Claude Code 后端(2 小时)——按官方 Channels(消息通道)的规范,把 Web Chat 做成一个自定义通道。通道本质上是一个 MCP 服务器(MCP 是让 AI 连接外部工具的通用协议),跟 Telegram 通道是同一套机制。有两个前提:自建通道不在官方白名单里,研究预览期要用 --dangerously-load-development-channels 参数加载;Channels 官方要求用 claude.ai 账号或 Anthropic Console 的 API Key 登录(详见 Channels 那篇)
  5. 本地跑通(1 小时)——浏览器打开 localhost:3300,发「你好」,bot 回「你好」

Day 1 结束:本地能聊。没样式、没鉴权、没部署。但核心通路打通了。

Day 2:鉴权 + 体感打磨 + 文件上传(8 小时)

Section titled “Day 2:鉴权 + 体感打磨 + 文件上传(8 小时)”

目标:别人不能随便打开,体验跟 ChatGPT 那种平滑。

我做的事:

  1. 加 PIN 鉴权(1 小时)——4 位数字 PIN,服务端检查,Cookie 记 30 天
  2. 优化交互体感(2 小时)——「正在思考」加载状态、reply 到达整段渲染、滚动定位、emoji 反应
  3. 加文件上传(2 小时)——拖拽上传(浏览器按 multipart 格式把文件发给服务器),服务端存进网关自己的数据目录
  4. 样式打磨(2 小时)——配色用 niuxue 主题暖橙,圆角 + 头像 + 间距细抠
  5. 响应式适配(1 小时)——手机 / 平板 / 桌面三档断点

Day 2 结束:本地体验干净、操作流畅,跟主流 chat 站接近(差距是没流式 token,这一档没做)。

Day 3:部署 + Cloudflare Tunnel + 公网(6 小时)

Section titled “Day 3:部署 + Cloudflare Tunnel + 公网(6 小时)”

目标:绑域名 chat.你的域名.com,走到哪都能用浏览器直接打开。

我做的事:

  1. 放进容器常驻(1 小时)——网关就是一个 Bun 进程,直接跑在 NAS 上已有的 claude-workspace 容器里,再写进容器的开机脚本,容器重启后自动拉起
  2. Cloudflare Tunnel 配域名(1.5 小时)——Cloudflare Tunnel 是家里的机器主动连到 Cloudflare 的一条隧道,不用在路由器上开端口;由 svc-tunnel 容器里的 cloudflared 负责,绑 chat.你的域名.com → localhost:3300
  3. DNS 配置(30 分钟)——在 Cloudflare DNS 加一条 CNAME 记录(把一个域名指向另一个域名的 DNS 记录),指向隧道
  4. HTTPS 自动(0 分钟)——Cloudflare 自动签证书,什么都不用做
  5. 端到端验证(2 小时)——在手机上 / 在公司网络 / 在咖啡馆 Wi-Fi,都能直接打开 chat.你的域名.com 进 PIN 用
  6. 开源到 GitHub(1 小时)——清理 secret、写 README、推上去

Day 3 结束:生产可用 + 开源仓库已发布。

架构图 → WebSocket gateway → Claude Code AI brain,三层

组件选型为什么
框架没用框架——Vanilla HTML + JS前端就一个 index.html 加一个 app.js,React 全家桶反而是包袱
样式手写 CSS,无 Tailwind一个文件搞定,改起来快
实时推送WebSocketbot 回复后 gateway 整段推给浏览器,渲染一次到位
上传原生 FormData + drag-drop不依赖任何库
组件选型为什么
运行时Bun(不是 Node)Bun 启动快、自带 TS 编译、内存少
路由手写 fetch 路由没用 Express,一串按路径判断的 if 就够用
AI 接口自己写的 Channels 通道插件跟 Telegram 通道同一套机制,本质是一个 MCP 服务器
存储SQLite(存在一个文件里的小型数据库)不用另装数据库服务,聊天记录全在一个文件里
组件选型为什么
容器Docker (host network)跟 claude-workspace 共一个容器
公网穿透Cloudflare Tunnel不开端口、零防火墙配置
域名 / DNSCloudflare反正都在 Cloudflare 一站式
HTTPSCloudflare 自动不用手动跑 certbot

总成本:

  • 域名 你的域名.com:已经有的(每年 ~100 元)
  • Cloudflare Tunnel:0 元
  • 运行成本(电费):忽略不计
  • 模型用量:跟现有 bot 一起算

绝对成本基本就是域名钱。

PIN 守门 + 网页聊天入口示意

整个 3 天,我没有手写过一行代码——全是跟 Claude Code 说人话,它写。

举几个具体瞬间:

我说:

“我要做一个网页 chat,前端简单,后端要能接 Claude Code 的 channel 协议,体验要像主流 chat 站那样平滑。建议技术栈?要简单、3 天能上线。”

Claude 给了 3 个方案对比(React+Express / Vue+FastAPI / Vanilla+Bun),列了每个的复杂度跟交付时间。我们选 Vanilla+Bun——Claude 自己分析说「800 行内的项目,框架是负债」。(最后整个项目写了三千多行,比当初估的多得多,不过前端确实一直没用框架。)

瞬间 2:Day 1 下午,WebSocket 双向通道

Section titled “瞬间 2:Day 1 下午,WebSocket 双向通道”

我说:

「Gateway 跟浏览器之间用 WebSocket 推送 inbound message,outbound 走 HTTP POST。给我最小可跑骨架。」

Claude 写了 30 行——server 端开 /ws upgrade、客户端 new WebSocket(...) + onmessage 渲染气泡。复制粘贴跑通,后续优化都基于这套骨架。

我说:

「做 PIN 鉴权。要简单——用户输 4 位 PIN,正确给 cookie 30 天,错的不给。别太复杂,不用 JWT 那种。」

Claude 写了一段很短的中间件——读 cookie、验 PIN,cookie 里存的是用 PIN 算出来的一串固定哈希值(hash,把内容算成一串固定长度的「指纹」)。当时觉得比我自己想得简洁多了;后来复盘才发现,这正是漏洞之一:没有只有服务器知道的密钥,4 位 PIN 一共只有 1 万种可能,对应的 cookie 能提前全部算出来,登录又不限尝试次数(见文末《一盆冷水》)。

瞬间 4:Day 3 下午,Cloudflare Tunnel

Section titled “瞬间 4:Day 3 下午,Cloudflare Tunnel”

我说:

「我想绑 chat.你的域名.com 到这个 server。我已经有 你的域名.com 这个域名 in Cloudflare。该怎么搞?」

Claude 生成了一份完整的 config.yml(cloudflared 配置)、写好了 DNS 记录的 CNAME、给了 docker-compose 启动命令。我抄过去 1 分钟搞定。

整个 Day 3 部署阶段——我做的事就是把 Claude 给的命令复制粘贴。

全过程踩的 3 个坑(分钟级排查)

Section titled “全过程踩的 3 个坑(分钟级排查)”

❌ 坑 1:WebSocket 连接被中间代理踢

Section titled “❌ 坑 1:WebSocket 连接被中间代理踢”

Day 2 测试时,WebSocket 偶尔连一会就断——连接一段时间没有数据往来,就会被中间的代理当成空闲连接关掉(后来接上的 Cloudflare 也会这么做)。

修复思路是心跳保活 + 断线重连:让连接隔一会儿就有一次心跳(服务端发 ping,浏览器自动回 pong),就不会被当成空闲连接;前端再兜个底,连接一断,2 秒后自动重连。

📚 Cloudflare 官方给的也是心跳这个办法,见它的 WebSockets 文档(面向开发者,偏技术)。

❌ 坑 2:Cloudflare Tunnel 第一次绑域名,撞上通配符记录

Section titled “❌ 坑 2:Cloudflare Tunnel 第一次绑域名,撞上通配符记录”

Day 3 绑 chat.你的域名.com 时,看到域名下已经有一条 *.你的域名.com 的通配符(wildcard,能匹配所有子域名)记录,以为 chat 会被它盖住。

其实通配符那条不用动:Cloudflare 里具体的记录优先于通配符,只有查询的名字下没有任何精确记录时,通配符才生效。直接给 chat 加一条指向隧道的 CNAME(橙云,也就是走 Cloudflare 代理)就行,也可以用命令 cloudflared tunnel route dns <隧道名> chat.你的域名.com 自动加。要是提示「记录已存在」,说明已经有一条同名的 chat 记录,删掉旧的那条再加。

千万别图省事把通配符改成「仅 DNS」(灰云):走通配符的子域会失去 Cloudflare 代理,指向源站 IP 的会直接暴露 IP,指向隧道的会直接打不开。先具体后泛化,DNS 本来就是这么查的(见 Cloudflare 的通配符记录说明,面向开发者,偏技术)。

Day 3 准备 push 到 GitHub 时,Claude 提醒:.env 里有 DeepSeek API key,要不要先清?

我说要,它自动 grep 了所有 secret 关键词,生成了一份 .gitignore,并建议加 git pre-commit hook 永久防止 leak。

这一脚没踩——但要是没 Claude 提醒,我可能就直接 push 了。

代码仓库已经删除,这里不再提供能直接部署的代码。原因在文末《一盆冷水》:当初的版本验证有漏洞,谁照着部署,谁的对话就可能被外人看到。

想给自己的 bot 做一个网页入口,能借鉴的是思路,不是代码:

  • 先把门做好再上线:所有接口都要先验证身份,包括实时消息通道;密码要够长,还要限制尝试次数。
  • 能不上公网就不上:只在自己的设备之间用(比如走家里的内网),比挂在公网上安全得多。
  • 上线前做一次安全检查:让 AI 逐个接口查一遍「外人不登录,能不能看到或发出消息」。
维度TelegramWeb Chat
入口装客户端 / 网页版直接打开网址
中国大陆访问无法直连免费套餐走境外节点,可能很慢或打不开,不作保证
文件上传机器人收文件上限 20MB,发文件上限 50MB自己定(当时设的是单个 50MB)
历史Telegram 服务器保存本地保存(隐私好)
多端手机 + 桌面任何浏览器
群组 / 频道✅ 原生支持❌ 没做
通知✅ 推送通知❌ 浏览器关了就没了

当时的实际用法:

  • 在家 / 公司:Web Chat(打开浏览器就能用)
  • 在外面 / 通勤:Telegram(手机锁屏推送)
  • 协作 / 分享:Telegram(可以转发)

「备用入口」这个思路本身没错:Channels 那篇讲过 Telegram 账号出状况、通道全断的故事,单一入口一倒,全家都找不到 bot。只是这个网页入口后来因为安全问题先一步停用了(见下面《一盆冷水》),Telegram 通道也在 2026-07 停用。如今我们家的日常入口是终端和 Claude 官方的 Remote Control(远程接管)——备用入口要留,但门得先做好。

最后说一个反直觉的事——这种「3 天上线」在 AI 时代之前是不可能的。

  • 2024 之前,我至少要花 1-2 周:学新框架、查 WebSocket 文档、配 Cloudflare Tunnel、调样式 bug…
  • 2026 现在:Claude Code 在每一步替我做研究 + 写代码 + 排错,我做的是产品判断 + push 节奏

这才是 AI 工具的真正价值——不是替代你思考,是替代你做「已知怎么做但费时间」的事。

普通人用 Claude Code 一周做出一个能用的小工具,是新常态,不是天才特权。

三周后的一盆冷水:它在公网裸奔

Section titled “三周后的一盆冷水:它在公网裸奔”

这篇案例原本到上一节就结束了。但后来发生的事,比「3 天上线」更值得写下来。

上线大约三周后,我们做了一次全家服务的安全巡检。查到 Web Chat 的时候,所有人沉默了——

它实际上没有任何有效的验证,就那么挂在公网上。

「等等,不是有 4 位 PIN 吗?」——有,但复盘发现它只拦住了页面这层门帘:后端接口是敞开的,知道路径的人可以绕过 PIN 直接跟服务说话。一道看着安心、实际不设防的门,比没有门更危险。

也就是说,知道地址的任何人,都能直接跟我们的 AI 说话:能看到聊天历史、能让它干活、能烧我们的 API 余额。它就这样裸奔了三个星期,只是运气好没被人撞到。

更麻烦的是,这种能动手干活的 AI 还怕一种更隐蔽的攻击,叫提示注入(prompt injection):把指令藏在发给 AI 的消息、文件或网页里,冒充主人或系统,骗它去做你没让它做的事,比如翻出聊天记录、交出密钥、在服务器上跑命令。入口谁都能进,等于谁都能来试这一招。

复盘下来,不是不懂,是“3 天上线”的兴奋压过了一个最朴素的问题:谁能访问它?

  • Day 1 到 Day 3 满脑子都是「跑起来」:消息能不能收发、界面顺不顺、隧道通不通
  • 测试全程是「自己打开自己用」——内网思维直接带上了公网
  • 上线那一刻的心情是「能用了!」,没有人问一句「陌生人打开会看到什么?」

这不是技术能力问题,是流程问题。AI 把开发速度提到 3 天,但没有替你做安全检查——那一步还是得人来问。

修复本身反而简单得可笑:给入口加了一道邮箱验证门(用的 Cloudflare Access,免费):

  1. 打开链接,先看到一个输入邮箱的页面
  2. 只有白名单里的邮箱能收到六位验证码
  3. 验证通过才能进聊天界面

配置全程十来分钟。三周的裸奔,十分钟就能避免——这才是最扎心的部分。

四条教训,做自己项目前先过一遍

Section titled “四条教训,做自己项目前先过一遍”
  1. 「能用」 ≠ 「能上线」。上公网前问一句:陌生人拿到这个链接,能看到什么、能做什么?答案让你心里一紧,就先别上。
  2. 加认证不是大工程。Cloudflare Access、Basic Auth 这类现成方案十来分钟就能接,别因为「回头再弄」跳过——「回头」往往是三周后。
  3. 定期扫一遍你的公网入口。项目做多了一定会忘,列个清单每月过一次:哪些东西暴露在公网、每个有没有门。
  4. 有了门,也要防提示注入。别人发来的消息和文件、AI 打开的网页,都可能夹带指令。只给 AI 干活必需的权限;删文件、往外发东西、花钱,要人点头才能做;给 AI 立个规矩:谁自称主人或系统、要它停手或交出设置和密钥,一律先拒绝,再找本人确认。目前还不清楚有没有万无一失的防范办法(安全组织 OWASP 的提示注入说明就是这么写的,英文),所以这几层要一起上。

你现在挂在公网上的东西里,哪一个是当时想着「先跑起来再说」的?现在就去打开它,用陌生人的眼睛看一眼。

接下来还有:

→ niuxue.org 是怎么搭起来的:本站自身案例

→ 一周两个 AI 小工具:工时表 + 墨水屏看板

→ MoldPage:把「找工作的痛」做成产品的两周

想及时看到更新,可以收藏 niuxue.org 主页。