技术深挖(二):Wails 3 跨端通信——前端如何优雅地“等待”扫码成功?

在完成了 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 的核心业务是“看广告换带宽”。这就带来一个挑战:用户点击“连接”后,由于没看广告,流程会中断并弹出小程序码。前端如何知道用户什么时候看完了广告? 拦截与判断 我们不能在前端写 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 中。对于这类工具软件,减少资源加载链比追求组件化更重要。 四、 结语 前端在 Wails 3 项目中扮演的是 “状态反馈器” 的角色。通过 Bindings 调用 Go 方法,通过 Events 监听实时动态。 ...

2025-12-04 · 2 min · Eagle

技术深挖(一):Wails 3 驱动 frp 客户端的核心实现与避坑指南

在上一篇文章中,我介绍了 Mole 客户端的业务闭环。今天,我们切入代码层面,聊聊基于 Wails 3 的 Go 后端是如何驱动 frp 核心并保证体验平滑的。 一、 Wails 3 项目启动架构 Wails 3 的启动逻辑高度模块化。在 main.go 中,一切从 application.New 开始。 1. 资产集成与服务绑定 首先,我们需要通过 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 个核心经验: ...

2025-12-03 · 2 min · Eagle

Frp 管理客户端:基于 Wails 3 的 frp 智能管理实战

Mole 是一款基于 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. 软件主界面 ...

2025-12-02 · 1 min · Eagle

系列开篇:告别命令行的束缚,我为何选择 Wails v3 打造 frp 管理客户端?

一、困境与契机:低配服务器、高性能需求与共享经济的思考 我的云服务器配置很“复古”: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 阶段,但它宣传的特性完美契合我的所有需求: ...

2025-12-01 · 1 min · Eagle

跨平台桌面开发新选择:Wails 3 初体验及在 FRP 管理客户端中的选型实践

为什么选择了Wails 3 ? Wails 3 最大的改变在于它不再强绑定于某个特定的前端框架,且引入了多窗口支持和更轻量级的 Runtime。它允许你在不启动主窗口的情况下运行后端服务,这正是我们实现“系统托盘”和“后台演示服务”的基础。 在 v2 中,我们习惯于自动生成的 wailsjs 文件夹。但在 v3 中,这一逻辑被进一步标准化。 当你运行开发指令时,Wails 会扫描你的 Go 结构体方法,并将其映射为前端可以调用的 JavaScript 函数。这个过程在 v3 中被称为 Generate 过程。 Wails 3 通信灵魂 我们在前端调用在后端定义的 HandleConnect 返回自定义结构体,这在 Wails 3 中是前后端通信的灵魂。 在 Wails 3 开发中,最核心的动作就是:后端做功,前端表现。 当你调用 MoleService.HandleConnect() 时,Go 后端会产生一个结果。在本项目中,我们需要同时返回一个 Code(状态码)和一个 Content(数据内容)。 为了实现这一点,我们定义了一个结构体: type Response struct { Code int; Content string } 虽然 Go 内部使用的是结构体,但前端 JavaScript 只能读懂 JSON 对象。Wails 3 内部会自动帮你完成这个“翻译”过程。 但是,如果你想让前端看到的字段名是小写的(例如 res.code 而不是 res.Code),你必须在 Go 结构体定义时加上“注解”。 type Response struct { Code int `json:"code"` Content string `json:"content"` } 记住,所有通过 bindings 调用的 Go 方法,在前端返回的都是一个 Promise 对象。这意味着你必须使用 await 或者 .then() 来接收数据,否则你拿到的将是一个永不开启的“盲盒”。 ...

2025-11-01 · 4 min · Eagle

私有化语音验证码方案:基于 Asterisk 与 Go 的 SIP 通信及 AGI 脚本实战

音频文件处理 我们使用手机上的录音机来录制音频文件。 Android 录音机录制的音频文件格式为 mp3,如果是 amr 格式,请使用豆子工具音频格式转换功能,转成 mp3 格式文件。 IOS 录音机录制的音频文件格式为 m4a,请使用豆子工具音频格式转换功能,转成 mp3 格式文件。 我们还需要使用 ffmpeg 将 mp3 文件转成 g711a 格式文件。这个 mp3 转 g711a 功能后续会集成到豆子工具中。 mp3 转 g711a 命令: ffmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw 使用 ffplay 播放测试 ffplay -i test.alaw -f alaw -ac 1 -ar 8000 将制作好的音频文件存放在 asterisk sounds 目录,就可以在拨号计划中使用 Playback 应用调用它了。 配置实时数据库 今天讲解 Asterisk 如何实时将 SIP 用户写入 sqlite3 数据库。 先定义数据库表结构,我当前使用的 PJSIP 协议。 CREATE TABLE ps_endpoints ( id VARCHAR(40) NOT NULL, transport VARCHAR(40), aors VARCHAR(200), auth VARCHAR(40), context VARCHAR(40), disallow VARCHAR(200), allow VARCHAR(200), direct_media varchar(5) check(direct_media in ('yes','no')), connected_line_method varchar(10) check(connected_line_method in ('invite','reinvite','update')), direct_media_method varchar(10) check(direct_media_method in ('invite','reinvite','update')), direct_media_glare_mitigation varchar(20) check(direct_media_glare_mitigation in ('none','outgoing','incoming')), disable_direct_media_on_nat varchar(5) check(disable_direct_media_on_nat in ('yes','no')), dtmf_mode varchar(20) check(dtmf_mode in ('rfc4733','inband','info')), external_media_address VARCHAR(40), force_rport varchar(5) check(force_rport in ('yes','no')), ice_support varchar(5) check(ice_support in ('yes','no')), identify_by varchar(10) check(identify_by in ('username')), mailboxes VARCHAR(40), moh_suggest VARCHAR(40), outbound_auth VARCHAR(40), outbound_proxy VARCHAR(40), rewrite_contact varchar(5) check(rewrite_contact in ('yes','no')), rtp_ipv6 varchar(5) check(rtp_ipv6 in ('yes','no')), rtp_symmetric varchar(5) check(rtp_symmetric in ('yes','no')), send_diversion varchar(5) check(send_diversion in ('yes','no')), send_pai varchar(5) check(send_pai in ('yes','no')), send_rpid varchar(5) check(send_rpid in ('yes','no')), timers_min_se INTEGER, timers varchar(20) check(timers in ('forced','no','required','yes')), timers_sess_expires INTEGER, callerid VARCHAR(40), callerid_privacy varchar(40) check(callerid_privacy in ('allowed_not_screened','allowed_passed_screened','allowed_failed_screened','allowed','prohib_not_screened','prohib_passed_screened','prohib_failed_screened','prohib','unavailable')), callerid_tag VARCHAR(40), `100rel` varchar(20) check(`100rel` in ('no','required','yes')), aggregate_mwi varchar(5) check(aggregate_mwi in ('yes','no')), trust_id_inbound varchar(5) check(trust_id_inbound in ('yes','no')), trust_id_outbound varchar(5) check(trust_id_outbound in ('yes','no')), use_ptime varchar(5) check(use_ptime in ('yes','no')), use_avpf varchar(5) check(use_avpf in ('yes','no')), media_encryption varchar(10) check(media_encryption in ('no','sdes','dtls')), inband_progress varchar(5) check(inband_progress in ('yes','no')), call_group VARCHAR(40), pickup_group VARCHAR(40), named_call_group VARCHAR(40), named_pickup_group VARCHAR(40), device_state_busy_at INTEGER, fax_detect varchar(5) check(fax_detect in ('yes','no')), t38_udptl varchar(5) check(t38_udptl in ('yes','no')), t38_udptl_ec varchar(20) check(t38_udptl_ec in ('none','fec','redundancy')), t38_udptl_maxdatagram INTEGER, t38_udptl_nat varchar(5) check(t38_udptl_nat in ('yes','no')), t38_udptl_ipv6 varchar(5) check(t38_udptl_ipv6 in ('yes','no')), tone_zone VARCHAR(40), language VARCHAR(40), one_touch_recording varchar(5) check(one_touch_recording in ('yes','no')), record_on_feature VARCHAR(40), record_off_feature VARCHAR(40), rtp_engine VARCHAR(40), allow_transfer varchar(5) check(allow_transfer in ('yes','no')), allow_subscribe varchar(5) check(allow_subscribe in ('yes','no')), sdp_owner VARCHAR(40), sdp_session VARCHAR(40), tos_audio INTEGER, tos_video INTEGER, cos_audio INTEGER, cos_video INTEGER, sub_min_expiry INTEGER, from_domain VARCHAR(40), from_user VARCHAR(40), mwi_fromuser VARCHAR(40), dtls_verify VARCHAR(40), dtls_rekey VARCHAR(40), dtls_cert_file VARCHAR(200), dtls_private_key VARCHAR(200), dtls_cipher VARCHAR(200), dtls_ca_file VARCHAR(200), dtls_ca_path VARCHAR(200), dtls_setup varchar(20) check(dtls_setup in ('active','passive','actpass')), srtp_tag_32 varchar(5) check(srtp_tag_32 in ('yes','no')), UNIQUE (id) ); CREATE INDEX ps_endpoints_id ON ps_endpoints (id); CREATE TABLE ps_auths ( id VARCHAR(40) NOT NULL, auth_type varchar(10) check(auth_type in ('md5','userpass')), nonce_lifetime INTEGER, md5_cred VARCHAR(40), password VARCHAR(80), realm VARCHAR(40), username VARCHAR(40), UNIQUE (id) ); CREATE INDEX ps_auths_id ON ps_auths (id); CREATE TABLE ps_aors ( id VARCHAR(40) NOT NULL, contact VARCHAR(40), default_expiration INTEGER, mailboxes VARCHAR(80), max_contacts INTEGER, minimum_expiration INTEGER, remove_existing varchar(5) check(remove_existing in ('yes','no')), qualify_frequency INTEGER, authenticate_qualify varchar(5) check(authenticate_qualify in ('yes','no')), UNIQUE (id) ); CREATE INDEX ps_aors_id ON ps_aors (id); PJSIP 测试数据: ...

2022-08-21 · 5 min · Eagle