起因:传个东西太烦了
你有没有这种时刻:
- 手机上看到一段文字,想发到电脑上——打开微信文件传输助手,等它加载,粘贴,发送,电脑端再等接收
- 想把 WiFi 密码从电脑发到手机——又是一轮微信登录
- 传个截图——微信压缩了,清晰度没了
- 想回头找三天前传的那个密码——聊天记录翻半天
这些场景的共同点是:我就是想把一条信息从 A 设备弄到 B 设备,为什么这么费劲?
市面上有 AirDrop,但只限苹果生态且要双方确认;有 Snapdrop/PairDrop,但不留历史记录;有 PrivateBin,但它是便签不是聊天流;有 Syncthing,但它是文件同步不是”发一条”。
没有一个工具正好干这件事:单人、跨设备、能存能查、像聊天框一样简单。
所以我造了一个。
速传是什么
一句话:你的私人跨设备中转站,界面是一个聊天框,在自己手机/电脑/平板之间传文字、密码、文件、截图。
它长这样:
- 多房间隔离:不再是「一个密码管全站」。开多个房间,每个房间独立时间流,可加密码可不加。默认有个
BUG反馈房间不加密,留给外部接口往里塞数据;自己日常用的房间可以加密 - 侧边栏房间列表:房间再多也不挤——桌面端左侧常驻侧边栏,移动端抽屉式滑出。切换房间、新建房间、管理工具都在侧边栏
- 删除房间:不要的房间连同全部消息一键删除(硬删,不进回收站)。需要输入服务端配置的
ADMIN_PASSWORD验证,错误密码有延迟 + 限速防护,默认房间不可删 - 消息置顶:常用密码、常用链接钉在房间顶部,不用翻历史。每个房间一条置顶,消息被删时自动解除
- 暗色模式:亮 / 暗 / 跟随系统三态切换,跟随系统时实时响应系统主题变化
- 回收站:删错的消息先进回收站,3 天内可恢复;3 天后自动彻底删除并清理对应文件。误删不再是不可逆操作
- BUG反馈 webhook:
BUG反馈房间有一个固定的 Bearer Token,外部脚本/服务可以直接通过 HTTP 把错误日志、截图、崩溃文件推到这个房间,相当于一个自托管的轻量级错误收集器 - 时间流单栏:所有消息按天分组,统一方向排列。不是聊天气泡那种左右分边——因为只有你一个人,没有对话对象,单栏扫读效率最高
- 长文字自动折叠:超过约 8 行折叠显示「展开全文」,不会一条长消息占满整个屏幕
- 图片直接预览:截图显示缩略图,点击全屏查看、可下载
- 粘贴/拖拽上传:桌面端 Ctrl+V 粘贴截图直接发送,拖文件进窗口即上传
- 历史搜索:顶部搜索框,多关键词模糊匹配(顺序无关),”admin 密码” 能命中 “密码是 admin”
- 复制按钮:每条文字消息带一键复制,传密码就是为了粘贴用
- 容量统计 + 清理工具:顶栏实时显示消息数和占用空间,支持按时间批量清理(7/30/90 天前)
- 版本号:侧边栏底部显示当前版本(v1.2.0),点击可看构建时间
核心设计决策是房间隔离模型——不需要账号体系,用房间名 + 可选密码把不同场景的内容隔开:日常传输房间加密、BUG反馈房间不加密但只接受带 Token 的外部推送、临时房间用完即弃。这让权限边界清晰,又比多用户系统简单一个数量级。
技术栈:为什么是 Go + Vue3
选型时我考虑过 Node、Python、Go,最终选了 Go 单二进制。原因很实际:
Go 的单二进制交付是杀手锏。 编译出来一个 15MB 的可执行文件,前端已经嵌在里面了。服务器上不需要装 Node、不需要装 Python、不需要装运行时——一个文件 + 一个数据目录,完事。升级就是替换二进制重启,迁移就是把数据目录拷走。
SQLite 做存储。 单用户量级,消息撑死几万条,SQLite 绰绰有余。用的 modernc.org/sqlite 纯 Go 驱动,不依赖 CGO,交叉编译零障碍。WAL 模式 + SetMaxOpenConns(1) 彻底避免 “database is locked”。
Vue3 + Vite 做前端。 构建产物只有 ~34KB(gzip),通过 Go 1.16+ 的 embed 包直接嵌进二进制。访问网址就是完整站点,没有前后端分离部署的麻烦,没有跨域问题。
Docker 三阶段构建。 第一阶段 Node 构建前端,第二阶段 Go 编译后端(把前端产物 embed 进去),第三阶段 Alpine 运行时——最终镜像只有二进制 + 基础系统,约 20MB。
整个技术栈的选择标准只有一个:能少一个依赖就少一个依赖。 个人工具不需要微服务、不需要 Redis、不需要消息队列。越简单越不容易坏。
安全模型:先想清楚再写代码
这个工具会存我所有的密码,安全设计不能含糊。核心决策:
房间级密码,而非全站密码。 每个房间可选是否加密。加密房间的密码不存明文——存的是 SHA-256 摘要,验证时比对摘要。登录成功后下发一个 room_session cookie(10 年有效)标记当前房间,下次进同房间免密。不加密房间(如 BUG反馈)直接进,但它的写入只接受两类来源:浏览器里的你,或带正确 Token 的 webhook 调用。
Webhook 用恒定时间比较。 BUG反馈 房间的 Token 用 subtle.ConstantTimeCompare 校验,避免时序攻击。Token 在部署时通过 WEBHOOK_TOKEN 环境变量注入,不写进数据库、不出现在前端。
高危操作独立管理密码。 删除房间是全站最高危的操作——连消息带文件一起硬删。它不走房间密码,而是要求输入服务端环境变量里的 ADMIN_PASSWORD(同样恒定时间比较)。这个密码只存在你的密码管理器里,不进数据库、不进前端;不配置这个变量,删除房间功能整体禁用。错误密码加 200ms 延迟 + 每分钟限 10 次。
必须走 HTTPS。 密码和消息明文存在 SQLite——这意味着服务器本身的安全就是密码库的安全。HTTP 明文传输会让密码在网络上裸奔,所以部署时必须配 nginx + Let’s Encrypt。
登录限速。 同一 IP 每分钟最多 10 次房间登录尝试,错误密码加 300ms 延迟拖慢暴力破解。
端口不直接暴露。 服务只绑 127.0.0.1:8787,公网通过 nginx 反代访问。即使有人扫到 8788 端口也连不上。
每日自动备份。 每天凌晨 4 点用 VACUUM INTO 生成数据库一致性快照,和上传文件一起打成 tar.gz,保留最近 7 份。运行中备份不影响服务。
没有做端到端加密——那是 PrivateBin 的路线。我选了明文存 + 服务器安全这条务实路线,因为端到端加密的代价是换设备前必须输对密码才能解密历史,实现复杂度明显上升。这个权衡你可以不认同,但它是我深思熟虑后的选择。
多房间 + 回收站:为什么这么设计
第一版只有一个全局密码、一条时间流。用了一阵发现两个真实痛点:
痛点一:内容混在一起。 BUG反馈、日常传密码、临时贴段代码,全挤在一个流里。想找三天前的密码得先翻过一堆报错日志。多房间就是解这个——BUG反馈 房间专门收外部推送,日常房间加密只自己看,内容物理隔离,搜索也按房间作用域。
痛点二:误删即丢失。 手机上手指一滑删错一条,没了就是没了。回收站是必须的。
实现上几个关键点:
- 单表 +
room_id列:没有给每个房间建一张表,那太重。所有消息都在messages表,加room_id外键隔离。查询带WHERE room_id = ?即可,索引覆盖,几万条量级毫秒级返回 - 软删除 + 自动清理:删消息不是
DELETE,是UPDATE ... SET deleted_at = ?。回收站查WHERE deleted_at IS NOT NULL,恢复就是清空deleted_at。后台 goroutine 每小时跑一次PurgeExpired,把deleted_at超过 3 天的消息彻底删掉,连带磁盘上的上传文件一起清掉——避免回收站变成只增不减的垃圾堆 - 房间密码存摘要:
password列存 SHA-256 hex,不存明文。即使数据库泄露,攻击者也拿不到原始密码(只能爆破) BUG反馈房间写死 ID=1:webhook 接口固定往 room ID=1 推,前端不显示这个房间的删除按钮(外部推送的数据由系统管理),避免误删外部上报的 BUG
BUG反馈 Webhook:把外部报错收进同一个流
BUG反馈 房间最有意思的地方:它不只是你在浏览器里看,还能被外部调用。
设想场景:你有个跑在服务器上的脚本、一个 webhook 钩子、或者一个监控告警——出了问题想留个痕。以前是发邮件、发钉钉、写日志文件。现在直接 POST 一条消息到速传的 BUG反馈 房间,和你手动贴的报错截图在同一个时间流里。
接口规范:
| 项 | 值 |
|---|---|
| 文本消息 | POST /api/webhook/message |
| 文件上传 | POST /api/webhook/upload |
| 鉴权 | Authorization: Bearer <WEBHOOK_TOKEN> |
| 内容类型 | application/json(文本)/ multipart/form-data(文件) |
| 落点 | 固定写入 BUG反馈 房间(room ID=1) |
文本消息示例:
1 | curl -X POST https://你的域名/api/webhook/message \ |
字段说明:
content(必填):消息文本type(可选):text(默认)或code。code类型前端会按等宽字体渲染,适合贴错误堆栈
文件上传示例:
1 | curl -X POST https://你的域名/api/webhook/upload \ |
字段说明:
file(必填):要上传的文件,支持任意类型。图片自动识别为图片消息(前端预览),其他文件作为附件下载content(可选):附带的文字说明,会作为独立文本消息插在文件之前
鉴权失败返回 401:
1 | {"error": "token 无效"} |
成功返回 200:
1 | {"ok": true, "id": 42} |
WEBHOOK_TOKEN 在 docker-compose.yml 的环境变量里设,部署后即用。想换 Token 改环境变量重启容器即可,老 Token 立刻失效。
我用它接了个 cron 脚本,每晚备份失败就推一条到 BUG反馈 房间——第二天打开速传,红色报错就在时间流最顶上,比翻邮件快多了。
踩过的坑
写这个的过程中踩了几个有意思的 bug,记下来:
1. SQLite NULL 列扫描失败
Go 的 database/sql 默认不能把 NULL 扫进 string。文本消息的 file_id、file_name 等列允许 NULL(因为文字消息没有文件),直接 Scan(&msg.FileID) 就报错。
修复:用 sql.Null[string] 接收,再取 .V 字段。或者更简单——查询时用 COALESCE(file_id, '') 把 NULL 转空字符串。
2. 空切片序列化成 null 导致前端崩溃
Go 里 var msgs []Message 是 nil 切片,json.Marshal 会把它序列化成 null 而不是 []。前端拿到 data.messages.length 就报 Cannot read properties of null。
修复:初始化为空切片 msgs := []Message{},保证序列化为 []。这是 Go 的经典坑。
3. 备份保留策略切片越界
1 | for _, n := range names[m.keep:] { ... } |
当文件数少于 keep 时,names[7:] 在长度只有 3 时直接 panic(slice bounds out of range)。经典 Go 切片越界。修复:先判断 len(names) > m.keep。
4. 手机键盘弹出把顶栏推走
这是最难的一个。最初用 scrollIntoView({ block: 'center' }) 想把输入框拉回键盘上方,结果在 overflow: hidden + flex 布局的页面上,浏览器找不到正确的滚动容器,把整个文档往上推——顶栏跑出屏幕、消息列表被挤到上面。
最终方案是不用任何 JS 滚动,纯靠布局自适应:
body设position: fixed; inset: 0,把页面钉死在视口里--app-heightCSS 变量跟随visualViewport.resize更新- 键盘弹出 → 视口缩小 →
#app变矮 → flex 列布局重算 →.list缩高 + 输入区钉底
全程零 JS 滚动,浏览器推不动页面,输入框自然在键盘正上方。
5. 中文输入法回车误发送
Vue 的 @keydown.enter 在中文输入法选词确认时也会触发,导致还没选完词消息就发出去了。修复:检查 e.isComposing || e.keyCode === 229,合成状态下不发送。
部署:三步上线
1 | # 1. 拿代码 |
启动后默认会自动创建 BUG反馈 房间(不加密,room ID=1),用 WEBHOOK_TOKEN 就能往里推消息。其他房间在网页里手动新建,可选是否加密码。
如果要用域名 + HTTPS,把 nginx.conf.example 里的域名改成你的,放到 nginx 配置目录,然后 certbot --nginx -d 你的域名 签证书。强烈建议走 HTTPS,否则房间密码和消息在网络上明文传输。
手机上打开网址,Safari 选「添加到主屏幕」,之后点图标即用,体验接近原生 App。
开源
代码已开源:github.com/7852604/transfer
MIT 协议,随便用。目录结构:
1 | transfer/ |
整个项目约 2000 行代码(Go ~1200 行 + Vue/CSS ~800 行),没有第三方前端 UI 库,没有后端框架——标准库路由 + 原生 SQL + 原生 Vue 组件。少即是多。
写在最后
这个工具我每天都在用。传密码、传截图、传一段代码片段、传一个文档——打开网址,粘贴,发送,另一台设备已经在了。再也不用等微信同步、不用担心文件被压缩、不用翻聊天记录找三天前的东西。
后来又加了多房间和回收站:BUG反馈和日常传输分房间隔开,删错的东西进回收站 3 天内能捞回来。最意外好用的是 BUG反馈 的 webhook——把服务器的报错直接推到同一个时间流里,第二天打开速传红色报错就在最顶上,比翻邮件、翻日志都快。
最近这轮打磨了三个体验细节:侧边栏房间列表(房间多了不挤)、消息置顶(常用密码钉在顶部不用翻)、暗色模式(晚上看不刺眼)。删除房间则配了独立的管理密码——高危操作值得一把单独的钥匙。
它不复杂,但它正好解决我的问题。有时候最好的工具就是你自己造的那个——因为它的每一个设计决策,都是为你的使用场景量身定做的。
如果你也有跨设备传输的痛点,试试看。有问题欢迎在 GitHub 提 issue。






