FRP Manager 是一款基于 Wails 3 开发的跨平台 frp 管理客户端。它不仅解决了 frp 配置繁琐的痛点,更探索出了一种“桌面工具 + 小程序生态”的全新闭环模式。
🌟 核心业务流程
Mole 的核心逻辑在于其自动化配置流与激励机制的融合:

1. 智能激励连接 (One-Click Connect)
当用户点击“一键连接”时,客户端会启动一套自动验证逻辑:
- 广告触发:系统检查用户当前状态。若未满足条件,则弹出动态窗口显示专属小程序码。
- 多端协同:用户扫码后进入微信小程序观看激励视频。
- 自动握手:用户观看完毕后,小程序通过后端指令反馈给 Mole 客户端。
- 静默取消:客户端接收指令后自动关闭弹窗,进入连接阶段。
2. 云端配置自动下发
Mole 不再需要用户手动编辑复杂的 TOML 文件:
- 动态获取:从服务器端安全获取分配的随机子域名和 frp 配置 Token。
- 本地注入:Go 后端自动将配置写入内置的
frpc.toml。 - 二进制调用:自动调用内嵌的
frpc核心组件启动服务。
3. 全链路事件反馈
利用 Wails 3 的高效事件机制,实现深度的 UI 反馈:
- 实时日志:Go 后端捕获 frpc 的标准输出,通过事件流(Events)实时推送到前端页面,用户可以清晰看到连接建立过程。
- 状态监控:实时反馈隧道运行状态,确保连接稳定性。
🛠️ 板块功能详情
- 连接控制台:一键启停,集成小程序激励流程。
- 端口配置页:灵活配置本地服务端口(如 8080, 3000 等),满足开发者调试需求。
- 实时日志页:直观展示 frp 运行日志,方便故障排查。
- 帮助中心:详尽的操作指南,降低内网穿透的使用门槛。
- 技术结缘 (About & Support):
- 集成优质云服务器推广(为用户提供可靠的 frp 服务端选择)。
- 扫码直达开发笔记,分享 Wails 3 与 Go 的底层实战。
👀 软件界面概览
1. 软件主界面

2. 小程序激励连接

3. 控制台

4. 配置

5. 日志

🏗️ 技术选型背后的思考
- Wails 3: 相比 Electron,我更看重其极小的包体积和对 Go 原生性能的直接调用。
- Go 驱动: 处理文件 I/O、进程管理及网络协议栈时,Go 表现出了惊人的开发效率。
- 小程序闭环: 避开了复杂的桌面支付系统,利用微信成熟的广告生态实现工具的良性循环。
下面我将讲讲它的来龙去脉。
一、困境与契机:低配服务器、高性能需求与共享经济的思考
我的云服务器配置很“低”:1 核 CPU、1G 内存、1M 带宽。它完美地满足了我个人博客的需求,直到我需要演示一套视频会议系统。该系统最低要求 2 核 4G。
我的服务器是包年做活动时购买,升级配置成本高昂,而我的本地电脑性能过剩,为了一次演示,我升级我的云服务器,我感觉不值。为了解决这个问题,frp(Fast Reverse Proxy) 成为了我的救星,它允许我将本地高性能服务安全地映射到公网。
在成功通过命令行 frpc 实现了内网穿透演示后,我发现了一系列生产力问题:
- 进程管理难题: 命令行窗口一旦关闭,
frpc进程随之终止,服务中断。这在日常使用中非常不便,因为会经常手误关闭窗口。 - 潜在的商业价值: 我有闲置资源服务器,公网 IP 以及域名。对于那些临时需要公网映射的用户来说,这是一个刚需。
- 流量变现与服务化: 我有一个小程序并集成了广告。如果能让用户通过看广告来获取临时的 frps 使用权(子域名分配),可以减少我的服务器支出,这将是一个双赢的模式。
为了解决这些问题并实现我的想法,我意识到可以做一个稳定、可后台运行、拥有友好 UI 的客户端,来替代原始的命令行操作。
二、技术选型坎坷路:Fyne、Tauri 的尝试与碰壁
我的技术栈主要集中在 Go、Rust 和 JavaScript,其中 Go 最擅长。我希望使用这些技术栈实现一个跨平台的客户端。
第一次尝试:Go & Fyne。Fyne 是一款使用 Go 语言编写的跨平台 GUI 库。我首先尝试了它。
- 优点: 纯 Go 编写,上手快。
- 痛点: 在处理 frpc 实时打印的日志时,Fyne 对中文字符的支持不理想;复杂的列表和表格布局实现起来很痛苦。最终放弃。
第二次尝试:Rust & Tauri。Tauri 是当时(也是现在)非常热门的跨平台框架,结合 Rust 的后端性能和 Web 前端技术。
- 优点: 性能强劲,打包体积小,前端界面开发体验好。
- 痛点: 我完成了界面设计,但在核心的 frp 控制模块上遇到了巨大的技术难题。Rust 的所有权(Ownership) 和生命周期管理让我头疼不已,始终无法优雅地实现 frp 进程的启动、停止和状态共享。项目卡壳。
三、柳暗花明:Wails v3 成为“天选之子”
在技术选型陷入僵局时,有读者告诉我可以尝试下 wails 3,我关注了 Wails 项目的 v3 版本。虽然现在它还处于 Alpha 阶段,但它宣传的特性完美契合我的所有需求:
- 极致轻量与原生性能: 生成的可执行文件体积小,内存占用低。
- 跨平台能力: 一套代码,多端运行,完美契合我的需求。
- 系统托盘支持: 这是我最核心的需求之一,允许应用在关闭主窗口后,静默在后台运行。
- Go 生态集成: Wails v3 基于 Go 后端,我可以使用我熟练的 Go 无缝地将 frp 管理,彻底解决 Tauri 中遇到的所有权问题。
熟读 Wails v3 Alpha 文档后,我兴奋异常。我将之前在 Fyne 和 Tauri 中已经梳理完善的业务逻辑和流程,迅速迁移到了 Wails v3 框架下。
利用 Go 的便利性,我成功实现了:
- 系统托盘的后台运行能力。
- 可视化配置管理(HTTP 映射),动态获取子域名和 frp 多用户 token。
- 一键启停 frp 客户端进程。
至此,一个功能完善的原型客户端基本完成。它优雅地解决了命令行和之前技术选型带来的所有痛点。
在这里,我想详细聊一下Wails3。Wails 3 的启动逻辑高度模块化。在 main.go 中,一切从 application.New 开始。
首先,我们需要通过 embed.FS 将前端生成物(Vite 构建后的 dist 目录)打包进二进制文件:
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "Mole",
Description: "Fast Reverse Proxy Manager",
Services: []application.Service{
application.NewService(&FrpService{}), // 绑定核心服务
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
})
// 创建窗口逻辑...
}
关键点: Services 是 Wails 3 的精髓。它是一个切片,支持绑定多个服务实例。每个服务中定义的公开方法,前端都可以通过自动生成的 JS 绑定直接调用。
在开发 Mole 的原型过程中,我踩了不少坑,总结出以下 5 个核心经验:
经验 1:系统托盘与“假关闭”逻辑。对于穿透工具,用户习惯点击“关闭”后应用依然在后台运行。我们需要在 application.WindowOptions 中将 DisableQuitOnLastWindowClosed 设为 true。然后监听 WindowsClosing 事件。当用户点击关闭图标时,调用 window.Hide() 而非销毁窗口。这样配合系统托盘(System Tray),可以实现应用的常驻运行。
经验 2:OnShutdown 与资源回收的“终点站”。最初我尝试在 Service 的 OnShutdown 中释放资源,但在实现托盘模式后,发现生命周期管理变得复杂。我是在 main.go 的应用级别配置 OnShutdown 回调。在这里统一清理 frp 进程、临时文件或断开云端连接,能确保程序退出时干干净净。
经验 3: ServiceStartup:预加载的黄金位置。每个 Service 都可以实现 ServiceStartup(ctx context.Context, options application.ServiceOptions) error 接口。在 Mole 中,我利用这个阶段完成以下任务:
- 读取本地 config.yaml。
- 从云端同步最新的子域名与 Token。
- 初始化内部状态机。
经验 4: 完美封装:Embed 编译与二进制调用。为了实现“单文件分发”,我将 frpc 原始二进制文件也通过 embed.FS 打包。在交叉编译时,根据目标平台(Windows/macOS/Linux)选择性打包对应的 frpc,避免生成物过于臃肿。静默运行(Windows 特供)优化,启动 exec.Command 时,必须添加 syscall.SysProcAttr{HideWindow: true}。这能彻底隐藏那个令人生厌的命令行黑窗口。
经验 5: 实时日志流:双 Goroutine 与事件机制。这是用户感知最强的功能。如何将 frp 的输出实时显示在前端?首先获取管道,通过 exec.Command 的 StdoutPipe 和 StderrPipe获取。然后并发读取,开启两个 Goroutine 分别读取这两个管道,防止阻塞。最后是事件推送,当读取到内容后,利用 app.Emit(“frp-logs”, content) 将数据推送到前端。前端只需要监听 frp-logs 事件,即可实现类似 Linux tail -f 的丝滑体验。
Wails 3 提供的 Services 和 Events 机制,极大地简化了 Go 与前端的通信。通过合理的生命周期管理(Startup/Shutdown)和进程控制,我们成功地为 frp 这个硬核工具披上了一层优雅的“外衣”。
此时我们将转战前端,聊聊如何用 JS 配合 Wails 事件流构建实时日志看板,以及如何实现那个关键的“小程序激励视频”弹窗交互。在完成了 Go 后端的硬核逻辑后,压力来到了前端。Mole 的前端并没有复杂的全家桶,而是回归本质,将精力集中在与 Wails 3 的运行时(Runtime)交互上。
在 Wails 3 中,前端不再需要通过 HTTP 接口访问后端,而是通过自动生成的 Bindings。在 main.js 中,这几行代码是整个应用的灵魂:
import { Events, Browser } from "@wailsio/runtime";
// 由 Wails 3 自动生成的绑定代码
import {
HandleStopFrp,
HandleStartFrp,
GetDomainURL,
} from "../bindings/mole/MoleService";
其中,Events实核心组件,用于监听后端的主动推送到前端的消息(如日志、状态变更)。HandleStartFrp 等它们看起来是 JS 函数,但执行时会直接触发 Go 后端对应 Service 的方法。
这样前后端就连接了起来。如果没有广告,那么开发工作就告一段落。但是开发 Mole 客户端的初衷之一,是希望探索一条个人开发者工具变现的路径。
四、关于集成广告变现的思考
我最初的想法是利用自己的 3M 带宽服务器,结合微信小程序的广告生态,实现一个“共享经济”的模式:
- 用户痛点: 临时需要公网 IP 、域名和稳定带宽进行本地调试。
- 我的资源: 闲置的服务器和域名资源。
- 解决方案: 用户在 Mole 客户端点击“连接”时,触发小程序激励广告。看完广告后,后端 API 自动为用户分配一个临时的、随机的子域名,并下发 frp 配置 Token。
这种 “桌面工具 + 小程序广告” 的模式,避开了复杂的桌面端支付系统,利用了微信成熟的广告体系。
当我准备将这个服务正式上线时,我意识到一个致命的风险:内容合规性。
frp 是一个中立的工具,但它提供的“内网穿透”能力具有双面性。如果用户利用我的服务器进行不法活动(如诈骗、传播违规内容),作为服务器的所有者,域名备案的所有者,我将承担直接的法律责任。服务器和域名随时可能被封,甚至可能面临法律风险。
对于个人开发者而言,这种风险是不可承受的。
为了规避风险,我做出了一个艰难的决定:放弃动态配置。我将客户端锁定为只能穿透由我指定的、本地启动的特定服务(例如一个我开发的本地 Web 服务器)。这样我能控制内容源,降低风险。但这导致了项目核心吸引力的丧失。用户使用 frp 是为了灵活性,锁定端口后,这个工具对懂技术的开发者来说毫无吸引力。项目陷入僵局。
既然提供“带宽服务”行不通,我决定回归“纯工具”本质。但我依然希望能产生收益。赞赏功能(Donation)是一个选项,但在竞争激烈的 frp GUI 市场,我的优势不大。
我找到了一个新的平衡点:将开发过程本身作为内容输出。我决定将项目坚持写完,并将整个开发过程、技术难点、踩坑经验整理成系列文章,发布到我的个人网站上。
Mole 项目的商业化之路虽然坎坷,但其衍生的价值却超出了我的预期。我不仅熟练掌握了 Wails 3、Go 进程管理、跨端通信等技术栈,还找到了一个可持续发展的方向。
我将继续开发一个公版(Generic Version)的 frp 管理工具——一个纯粹的、无商业绑定的、优雅的 frp GUI 客户端,并继续分享我的开发笔记。
在演示版中还有一个极其重要的环节没有交代:服务端(frps)的架构设计。如果说 Mole 客户端是用户看到的“门脸”,那么服务端就是支撑多用户安全、有序运行的“大脑”。即便在未来的纯工具版中不再强制使用我的服务器,但这套多用户鉴权与动态域名的方案,依然值得每一位开发者备忘。
fp-multiuser:实现多用户隔离的关键。在服务型工具中,不可能让所有用户共享一个 Token,否则无法追踪流量,也无法实现精准的权限控制。我选择了 frp 官方推荐的插件方案:fp-multiuser。它的核心逻辑是基于 OpLogin 事件的鉴权
。fp-multiuser 实际上是一个基于 HTTP 协议的外部插件。它的精妙之处在于:当 frpc 尝试连接 frps 时,插件会拦截相关事件。
在我的实现中,我重点使用了 OpLogin 事件:
- 分配 Token:当用户通过 Mole 客户端(或小程序激励后)请求连接时,后端 API 会动态生成一个唯一的
Token并下发给客户端。 - 校验映射:当
frpc发起登录,fp-multiuser插件会接收到这个Token。插件通过查表或调用我的管理接口,确认该Token是否合法、对应哪个子域名。 - 唯一映射:这样就确保了 A 用户只能使用 A 子域名,彻底解决了多用户环境下域名冲突和越权访问的问题。
还有一个就是泛域名证书:让每个子域名都拥有 HTTPS。演示版支持用户通过 HTTPS 访问本地服务。面对随时可能生成的成百上千个二级域名(如 user1.example.com, user2.example.com),手动配置证书显然是不现实的。我使用了 Let’s Encrypt 的泛域名证书。通过 DNS-01 验证方式(利用 Certbot 或 acme.sh)申请 *.example.com 的证书。一个 .pem 文件即可覆盖所有二级域名,无需为每个新用户重新申请。
最后,需要通过Nginx 反向代理配置打通链路。在服务端,我并没有让 frps 直接监听 443 端口,而是将其置于 Nginx 之后。Nginx 监听 443 端口,配置泛域名证书。利用 Nginx 的变量特性,将所有子域名的流量统一转发给 frps 的 vhost 端口。
server {
listen 443 ssl;
server_name *.example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080; # frps 的 vhost_http_port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
通过这种方式,客户端只需配置简单的 HTTP 映射,而在公网访问时,用户看到的是受保护的 HTTPS 链接。
最后,我还想说下在集成小程序广告时,如何实现?它以前的核心业务是“看广告换带宽”。这就带来一个挑战:用户点击“连接”后,由于没看广告,流程会中断并弹出小程序码。前端如何知道用户什么时候看完了广告?
- 拦截与判断
我们不能在前端写 if(adWatched),因为前端代码是透明的。逻辑必须在 Go 后端:如果后端判断该用户需要看广告,HandleStartFrp 会返回特定的状态码(如 2)。 - 状态机:从“连接中”到“等待验证”
看看这段核心的 connect 逻辑:
async function connect() {
const hint = document.getElementById("hint");
try {
const res = await HandleStartFrp(); // 发起 Go 调用
if (res.code === 1) {
// 情况 A: 验证通过,直接分配域名并连接
document.getElementById("subdomain-url").innerText = res.content;
setUIConnected(true);
addLog("成功连接到服务器", "system");
} else if (res.code === 2) {
// 情况 B: 触发激励逻辑,弹出小程序码
showAdModal(res.content); // res.content 包含小程序码 Base64 或 URL
addLog("请扫码完成验证后继续", "info");
// 【关键】注册一次性监听事件,等待后端“发令枪”
const unsubscribe = Events.On("ad-status", (data) => {
closeAdModal(); // 关闭弹窗
unsubscribe(); // 销毁监听,防止重复触发
if (data.status === "done") {
addLog("验证成功,正在连接...", "system");
connect(); // 再次发起连接请求,此时后端将通过验证
} else {
// 处理异常(如超时、未看完)
hint.innerText = data.message;
hint.classList.add("error-shake");
addLog(`验证未完成: ${data.message}`, "error");
setUIConnected(false);
}
});
}
} catch (err) {
setUIConnected(false);
}
}
为什么这样设计?(经验总结)
- 安全性(Security First):
连接逻辑的“入场券”完全由 Go 后端控制。前端只是一个 UI 展示层,即便用户强行修改 JS 调用 connect(),如果后端没有收到广告系统的回调信号,依然不会分配 frp 配置。 - 订阅-发布模式(Events):
使用 Events.On 而不是轮询(Polling)。当用户在手机上看完广告,后端 API 收到微信的回调后,会通过 app.Emit(“ad-status”, …) 通知前端。这种实时性让用户体验非常丝滑——手机看完,电脑屏幕上的弹窗瞬间自动消失并连接。 - UI 细节:单页面布局(Zero-Config UI):
由于 Mole 的界面追求极致精简,所有的 CSS 样式、HTML 布局和 JS 逻辑都高度集成在 index.html 和 main.js 中。对于这类工具软件,减少资源加载链比追求组件化更重要。
至此,FRP管理客户端从“为何而生”到“后端驱动”再到“前端交互”的开发逻辑已经全部分享完毕。这种 “桌面工具 + 小程序生态” 的模式,为个人开发者如何平衡“服务器成本”与“用户体验”提供了一个全新的思路。