☰
用Go和Vue3基于Wails v2构建轻量AI桌面应用实战
2026/11/4 9:53:47 网站建设 项目流程

用 Go 和 Vue3 做一个本地运行、随时打开的 AI 桌面应用,听起来是件挺酷但有点复杂的事。我在几个候选技术方案里折腾了一圈,最后选择了 Wails v2,把之前用 Electron 写的一个 AI 聊天客户端重写成了单二进制程序,安装包体积从 180MB 一路压到 15MB 以内,内存占用也下降了将近一半。这篇文章就把整个实战过程完整写出来:为什么选 Wails、前后端桥接层怎么设计、流式对话怎么做、以及我在打包和发布过程中踩过的那些坑。如果你正打算做桌面端的 AI 工具,或者想把自己的 Web 服务包一层桌面壳,这篇内容应该能帮你少走不少弯路。

1. 为什么我放弃 Electron,用 Wails v2 重写 AI 桌面客户端

1.1 先说说我原来那个"能跑但很重"的 Electron 版本

我最早做 AI 桌面客户端时,和大多数人一样选了 Electron,理由很直接:生态成熟、资料多、前端技术栈随便挑。但真正用起来之后,问题就慢慢浮出来了。首先就是安装包体积,一个带 node_modules 的 Electron 应用,随便打出来都要 150MB 往上,用户下载成本很高。其次是内存占用,我那个应用打开后常驻内存轻松超过 500MB,对话框一多、Markdown 渲染一复杂,风扇就开始转。最难受的是多窗口场景下的崩溃恢复,Electron 的 renderer 进程一旦内存吃紧,整个应用白屏的几率不小。

你说 Electron 完全不能用吗?也不是。它的 Chrome 内核渲染能力确实强,各种 Node.js 生态库随便调。但对一个 AI 聊天工具来说,我需要的其实就是一个本地 Shell:接大模型 API、管理会话记录、渲染 Markdown、再给用户一个体面的窗口。Electron 能做的事太多了,多到大部分时间都在处理它带来的额外复杂度,而不是在处理业务本身。

1.2 Wails v2 的取舍逻辑:单二进制、低内存、原生能力

后来我接触到了 Wails v2,它的核心设计非常对我的胃口:用 Go 写后端逻辑,用系统自带的 WebView 渲染前端界面,最后打包成一个不依赖运行时的可执行文件。Windows 上它走的是 WebView2,macOS 上走 WKWebView,也就是说不需要把 Chromium 整个塞进安装包。

用 Wails v2 重写之后,我的安装包从 180MB 降到了 14MB 左右,安装完打开应用,静态内存占用大约 120MB 到 180MB,比之前好了太多。而且 Go 编译出来的是一个原生可执行文件,启动速度肉眼可见地快,双击到窗口出现基本在 1 秒内。对于 AI 对话这种高频打开、用完就关的桌面工具,这个体验差异是决定性的。

同时 Go 后端在并发处理上天然有优势。大模型 API 的流式响应、多会话并发管理、本地向量检索这类 IO 密集操作,Go 的 goroutine 模型写起来比 Node.js 的事件循环要直白,也比 Python 的 GIL 舒服很多。我做 AI 应用需要处理大量 HTTP 长连接和并发请求,Go 确实是更顺手的选择。

1.3 Tauri 也看了,为什么最后选了 Wails

我知道肯定有人会问:Tauri 不也是轻量桌面方案吗?确实,Tauri 的方案和 Wails 很接近,都是系统 WebView + 后端逻辑,但我实际体验下来,发现两个框架的侧重点不一样。Tauri 的 Rust 后端性能更强,但对我来说有个比较大的学习成本问题:我不太想在 AI 应用这个阶段同时兼顾 Rust 的所有权模型和异步运行时,调试起来心里没底。

Wails 的 Go 后端就没有这个心理负担,Go 的语法简单,标准库的 net/http、context、encoding/json 足够我完成 90% 的工作,剩下的用几个常用库就能搞定。另外 Wails v2 的绑定机制非常直接,Go 方法可以自动暴露到前端,前端直接用 JavaScript/TypeScript 调用,不需要像 Tauri 那样在 command 和事件之间来回做映射。对优先保证开发效率、又不想牺牲应用体积的团队来说,Wails v2 确实是一个被低估的选择。

2. 环境准备与脚手架:Wails v2 + Vue3 的工程结构一次讲清

2.1 依赖安装和版本要求

在开始之前,先确认你本机的软件环境。当前 Wails v2 对 Go 版本有要求,建议用 Go 1.21 以上的版本,Vue3 项目依赖 Node.js,我使用 Node.js 18 LTS 或更高版本。Windows 下还需要 WebView2 Runtime,Win11 和大部分 Win10 系统已经内置,没有的话在微软官网下载安装即可。

具体来说,我当前的推荐版本组合是这样的:

组件推荐版本说明
Go1.21+Wails v2 需要较新的语言特性,建议用官方最新稳定版
Node.js18 LTS 或 20+Vue3 + Vite 构建,版本别太老
Wails CLIv2.10+安装和打包都靠它,虽然后续有 v3,但 v2 生态更成熟
WebView2最新 RuntimeWindows 的渲染内核,确认系统已安装

安装 Wails CLI 的命令是:

go install github.com/wailsapp/wails/v2/cmd/wails@latest

安装完跑一下wails doctor,它会检查所有依赖环境并给出缺失项提示。我第一次在 Windows 上执行时,就是靠这个命令发现缺少了 WebView2 的 SDK 依赖,补装后就好了。

2.2 wails init 初始化一个 vue-ts 模板

Wails v2 提供了多种前端模板,包括 React、Vue、Svelte 等,我这里用vue-ts模板,因为 TypeScript 在 AI 应用里确实能减少不少低级错误,尤其面对流式响应这种数据格式变化频繁的场景。

wails init -n ai-desktop -t vue-ts

这条命令会生成一个标准项目骨架。我建议初始化完成之后先跑一次wails dev,确认开发模式能启动。它会自动编译 Go 后端、启动 Vite 开发服务器,然后把整个界面嵌入到 WebView 窗口里。第一次跑会花一些时间,因为需要下载前端依赖和编译 Go 代码,属于正常现象。

wails dev还有个我很喜欢的能力:热更新。前端代码由 Vite 负责热更新,Go 代码改动后会自动重新编译并重启应用。调试阶段基本不需要手动关窗口再开,体验很像开发普通 Web 应用。

2.3 目录结构与配置文件逐个说明

项目初始化后,核心目录结构大致是这样:

ai-desktop/ ├── frontend/ # Vue3 前端项目(Vite 构建) │ ├── src/ │ │ ├── components/ │ │ ├── App.vue │ │ └── main.ts │ ├── package.json │ └── vite.config.ts ├── main.go # Wails 应用入口 ├── app.go # 后端服务结构体,绑定的方法都写在这里 ├── wails.json # Wails 项目配置 └── go.mod # Go 模块定义

main.go的职责是创建应用实例并声明要绑定的后端服务;app.go才是你写业务逻辑的地方。前端所有的 Go 接口调用,最后都会落到app.go中导出方法上。

wails.json里可以配置应用窗口大小、标题、前端构建目录等信息。比如我设置了一个相对宽润的默认窗口尺寸:

{ "name": "ai-desktop", "outputfilename": "aidu", "frontend:build": "npm run build", "frontend:install": "npm install", "wailsjsdir": "./frontend/wailsjs", "assetdir": "frontend/dist", "info": { "productName": "AI Desktop", "productVersion": "1.0.0", "copyright": "Copyright 2024" } }

需要注意的是wailsjsdir这个字段,它指定了前端需要的自动生成 JS 和 TS 类型定义文件的存放位置。第一次启动开发模式后,这个目录会自动生成,后面我会说到它的作用。

3. 桥接层设计:Go 方法与 Vue3 组件的通信机制

3.1 bind 绑定原理:Go 方法如何暴露给前端

Wails v2 的核心机制是绑定。在main.go中,你需要实例化自己的 App 结构体,并把它传给wails.New创建应用时的Bind配置项:

func main() { app := NewApp() err := wails.Run(&options.App{ Title: "AI Desktop", Width: 1024, Height: 768, AssetServer: &assetserver.Options{ Assets: assets, }, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, OnStartup: app.startup, Bind: []interface{}{ app, }, }) if err != nil { println("Error:", err.Error()) } }

所有在App结构体上、且可导出的方法都会自动暴露给前端。比如我在app.go里定义了一个获取会话列表的方法:

func (a *App) GetConversations() []Conversation { return a.store.List() }

前端 TypeScript 里会自动生成对应的调用代码,实际使用是这样:

import { GetConversations } from "../../wailsjs/go/main/App"; const conversations = await GetConversations();

这里要注意方法名的映射规则:Go 的GetConversations会直接生成一个同名函数,参数和返回值也会同时生成 TypeScript 类型。这就意味着,只要后端定义好结构体,前端的类型安全就能自动得到保证,不需要手写接口文档。

3.2 事件系统:后端主动推送流式内容给前端

桥接层另一块重要的内容是事件系统。文档里叫 Event 系统,它承担的是"后端主动叫前端"的场景。

在做 AI 对话时,最典型的场景就是流式输出:后端在收到大模型的部分回复后,需要不断把这些碎片推送到界面上,让用户看到打字机效果。HTTP 请求在 Go 后端里持续接收,但如果只是等待全部完成再一次性返回,用户会面对很长的空白等待时间。

所以我采用的方式是:后端每收到一段增量内容,就用runtime.EventsEmit推给前端,前端用EventsOn监听并即时渲染。

import ( "github.com/wailsapp/wails/v2/pkg/runtime" ) func (a *App) sendChunk(sessionID string, content string) { runtime.EventsEmit(a.ctx, "ai:chunk", map[string]interface{}{ "session_id": sessionID, "content": content, "done": false, }) } func (a *App) sendDone(sessionID string) { runtime.EventsEmit(a.ctx, "ai:chunk", map[string]interface{}{ "session_id": sessionID, "done": true, }) }

前端在使用时,需要在onMounted里注册监听,在onUnmounted里取消监听,避免组件销毁后事件仍然触发导致内存泄漏:

import { EventsOn, EventsOff } from "../../wailsjs/runtime/runtime"; onMounted(() => { EventsOn("ai:chunk", (payload) => { if (payload.session_id !== currentSessionId.value) return; if (payload.done) { streaming.value = false; } else { message.value += payload.content; } }); }); onUnmounted(() => { EventsOff("ai:chunk"); });

3.3 用 wails generate 生成前端类型定义

刚才讲到,wailsjsdir指定的目录会在开发模式下自动生成前端调用代码。如果你想手动重新生成,可以执行:

wails generate module

这个命令会在frontend/wailsjs目录下生成三个子目录:go存放后端方法的调用封装,runtime存放事件和窗口相关 API。生成的.d.ts文件包含了所有绑定方法的 TypeScript 类型定义,IDE 里直接有自动补全和类型检查,改完 Go 后端结构体以后,前端编译期就能发现接口不匹配的问题,这比我之前写 Electron 时在 IPC 通信里手维护协议要省心得多。

有一点需要注意:如果在原有生成的调用代码上新增了方法,开发模式热更新不一定每次都完整重新生成,当你发现前端没有出现新方法时,手动执行一次wails generate module再重启wails dev就好。

4. AI 对话核心实现:从普通 HTTP 请求到流式打字机

4.1 模型接入选择:兼容 OpenAI 接口的云服务还是本地模型?

AI 桌面应用最核心的功能就是对话。我这里的做法是把所有模型供应商都封装在同一套接口语义后面,这样前端只需要关心"发送消息"和"接收增量"两个动作,不需要知道具体接的是哪家服务。

对接云端大模型时,我会使用那些兼容 OpenAI Chat Completions 协议的服务,因为这几乎是事实标准。Go 后端可以直接用标准net/http发起 POST 请求,设置stream: true之后,服务端会以 SSE 格式不断返回data:前缀的行。

如果你对隐私性要求高,希望完全离线运行,也可以接本地 Ollama 服务。Ollama 本身就提供兼容 OpenAI 的/v1/chat/completions接口,地址是http://localhost:11434,区别只是模型名称、请求延迟和上下文窗口大小。我的应用里把"服务地址"做成了设置项,云端和本地可以随时切换。

方案优点缺点
云端大模型 API模型强、上下文长、不用自己买 GPU有网络依赖,数据经过第三方
本地 Ollama 模型离线可用,隐私可控需要显卡资源,模型体积大,效果稍弱

实际开发中我建议先接云端 API 保证功能跑通,再预留本地模型切换能力,这样即便用户没有 API Key,也能用本地小模型快速体验。

4.2 后端流式请求的完整实现:context、bufio、channel

Go 后端发起流式请求时,核心点在于如何逐行解析 SSE 数据。直接用标准库就能完成,不需要引入额外的 SSE 客户端。下面这段代码是我项目里的核心方法,做了简化但保留了关键逻辑:

func (a *App) ChatStream(sessionID string, userMessage string) error { a.ctx = context.Background() ctx, cancel := context.WithCancel(a.ctx) defer cancel() reqBody := map[string]interface{}{ "model": a.currentModel, "messages": []map[string]string{ {"role": "system", "content": a.systemPrompt}, {"role": "user", "content": userMessage}, }, "stream": true, } body, _ := json.Marshal(reqBody) req, err := http.NewRequestWithContext(ctx, "POST", a.apiBase+"/v1/chat/completions", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+a.apiKey) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) scanner.Split(scanLinesFunc) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data:") { continue } data := strings.TrimSpace(strings.TrimPrefix(line, "data:")) if data == "[DONE]" { a.sendDone(sessionID) return nil } var chunk struct { Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` } if err := json.Unmarshal([]byte(data), &chunk); err != nil { continue } if len(chunk.Choices) > 0 { a.sendChunk(sessionID, chunk.Choices[0].Delta.Content) } } return scanner.Err() }

这里有一个容易被忽略的关键点:Scanner 默认按行拆分,但 SSE 数据可能用\r\n结尾,也可能一行很长。我用自定义的split函数处理了\r的情况,避免最后解析出来的内容带奇怪的回车符。另外,http.NewRequestWithContext非常重要,它让后续的取消操作能真正中断底层 HTTP 请求,否则用户点击"停止生成"时,Go 协程会继续挂在那里阻塞。

4.3 前端状态机设计:idle、loading、streaming

流式对话的前端不能只用一个loading布尔值,因为用户需要知道当前是"等待首包"、"正在接收中"还是"已结束"。我定义了一个联合类型来描述消息状态:

type MessageStatus = "idle" | "loading" | "streaming" | "done"; interface ChatMessage { id: string; role: "user" | "assistant"; content: string; status: MessageStatus; error?: string; }

流程上,用户点击发送后,先在本地插入一条role: "user"的消息,再插入一条空内容的role: "assistant"消息,状态设为loading。后端接流后,通过事件推送把内容累积到这条 assistant 消息上,状态切为streaming。收到done事件后,状态切为done。

这样设计的好处是,UI 可以针对不同状态做差异化展示:loading状态显示一个三点动画,streaming状态显示光标或打字机特效,done状态则触发 Markdown 渲染。Chat UI 的滚动处理也依赖这个状态机——只有streaming状态下才自动滚到底部,loading和idle都不滚动,避免用户回看历史消息时被强行拉下去。

4.4 取消、重试与会话管理

流式请求最大的问题是网络中断和用户主动取消。我在App结构体里维护了一个sync.Map来保存每个会话对应的cancelFunc,当用户点击停止按钮时,调用CancelChat方法触发对应的 cancel,同时给前端发送一个"生成已中断"的事件。

func (a *App) CancelChat(sessionID string) { if fn, ok := a.cancelFuncs.Load(sessionID); ok { fn.(context.CancelFunc)() a.cancelFuncs.Delete(sessionID) runtime.EventsEmit(a.ctx, "ai:chunk", map[string]interface{}{ "session_id": sessionID, "interrupted": true, "done": true, }) } }

会话管理的思路也比较直接:所有会话和消息都放在 SQLite 里,通过一个 Repository 接口访问。Go 后端启动时初始化数据库,前端所有操作都通过后端方法进行,数据持久化放后端,避免前端直接操作本地文件。

我建议用crypto/rand生成会话和消息 ID,而不是依赖自增 ID,因为桌面应用会有大量本地写入,自增主键在迁移和同步场景下容易出问题。

5. 界面落地:Vue3 组件库选型、Markdown 渲染与暗色模式

5.1 组件库怎么选:Naive UI 还是 Element Plus?

Vue3 项目的 UI 组件库,目前主流是 Element Plus 和 Naive UI。Element Plus 生态大、组件全,适合后台管理系统;Naive UI 的主题系统更灵活,暗色模式切换做得很顺手,而且 TypeScript 支持更好。

我的 AI 桌面应用需要大量自定义聊天气泡、长文本和流式内容,对组件的定制需求很强,所以选择了 Naive UI。它的n-config-provider配合n-global-style可以一键切换亮暗模式,暗色模式下聊天界面的观感比亮色好很多,也更符合 AI 工具的调性。

当然,如果你只是想要现成的后台模板,Element Plus 的成熟度会更高。但做 AI 聊天界面,我建议还是从 Naive UI 或者完全手写组件开始,因为聊天 UI 的特殊交互(消息流、自动滚动、复制按钮、Markdown 代码块工具栏)大部分组件库都不会提供现成解决方案。

5.2 Markdown 流式渲染的性能问题

AI 回复的内容通常带有 Markdown 格式,代码块、表格、列表都需要优雅展示。我用的库是marked+highlight.js。但这里有个问题:流式输出过程中,如果每个 chunk 都做一次完整的 Markdown 解析和渲染,前端会非常卡。

我的优化策略是:流式过程中只显示纯文本,用white-space: pre-wrap保持换行,不对 Markdown 做完整解析。当done事件到达、内容不再变化时,才执行一次marked.parse生成最终的 HTML。这样做的好处是中间过程渲染开销很小,最终展示又保留了格式。唯一的代价是打字机过程中代码块没有高亮,但用户大多数时间更在意的是流畅度。

如果是长文档场景,还可以加一层防抖,200ms 内的多个 chunk 合并到一次渲染任务。实测下来,一次流式回复几百个 chunk 的情况下,页面没有明显掉帧。

5.3 暗色模式与窗口样式的心得

Wails v2 的窗口背景色可以在main.go的BackgroundColour中设置。做暗色模式时,我在前端用 Naive UI 的darkTheme和themeOverrides分别管理明暗两套配置。真正的难点在于:切换主题时,WebView 背景需要和窗口背景保持一致,否则窗口缩放时有白边闪动。

我的做法是:在 Vue3 的主题切换函数里,额外调用一个后端方法,动态更新窗口背景色和标题栏颜色:

import { SetTheme } from "../../wailsjs/go/main/App"; function applyTheme(theme: "light" | "dark") { document.documentElement.setAttribute("data-theme", theme); SetTheme(theme); }

Windows 11 下还可以配合 WebView2 的设置调整标题栏和应用图标。App.SetTheme里通过系统 API 修改窗口的DWMWA_USE_IMMERSIVE_DARK_MODE属性,让标题栏也跟随暗色主题,比单纯改网页背景协调得多。

6. 打包发布与体积控制:从开发机到用户桌面的最后一公里

6.1 wails build 的产物与资源文件处理

开发完功能后,执行wails build会生成最终的可执行文件。它会自动先构建前端静态资源,再将资源嵌入 Go 二进制中。默认输出目录是build/bin/,文件名由wails.json里的outputfilename决定。

这里有个常见的坑:如果你的前端代码中有对"绝对路径"的请求,比如/assets/xxx.js,在 WebView 里可能没问题,但打包后 Wails 的AssetServer只接受相对路径./assets/xxx.js,否则会返回 404。我在开发模式一直没发现问题,打包后界面空白,排查很久才发现是 Vite 的base配置没改成"./"。

解决办法是在vite.config.ts中设置:

export default defineConfig({ base: "./", plugins: [vue()], });

6.2 使用 ldflags 裁剪与 UPX 压缩

Go 的二进制默认包含大量调试信息,打包时通过编译参数可以减少体积。我在wails build时习惯加这样一组 ldflags:

wails build -ldflags "-s -w -H windowsgui"

-s去掉符号表,-w去掉 DWARF 调试信息,-H windowsgui让程序运行时不弹出控制台窗口(仅 Windows 生效)。搞定后这个 exe 大约 14MB 到 18MB,视前端资源大小而定。

如果想进一步压缩,可以使用 UPX:

upx --best --lzma build/bin/aidu.exe

UPX 压缩后体积能再降到 8MB 到 10MB。注意杀毒软件有时候会误报 UPX 加壳程序,这个打包时要有心理准备,看目标用户群体再决定是否使用。我的经验是,个人工具软件用 UPX 没问题,但面向企业分发时尽量别加壳,减少 IT 部门安全策略的误判。

6.3 NSIS 安装包与自动更新思路

要给普通用户分发,单文件 exe 不太友好,我建议打成安装包。Windows 下我用 NSIS 做了一个简单安装脚本,配置了桌面快捷方式、开始菜单项和卸载入口。NSIS 脚本本身不复杂,把生成的 exe、图标和许可协议塞进去,编译出来一个 5MB 左右的安装包。

关于自动更新,Wails 本身没有内置 update 机制,但你可以基于 Go 的github.com/inconshreveable/go-update实现。思路是启动应用后请求一个版本清单 JSON,发现有新版本就提示用户下载更新包,下载完成后替换当前 exe 再重启。如果暂时不想做自动更新,至少把版本号写进wails.json的info.productVersion,后续排查问题会顺畅很多。

7. 实战中的踩坑记录与性能优化心得

7.1 踩坑:前端取消请求导致 Go 协程泄漏

这个问题是我在测试"停止生成"按钮时发现的——点击停止后,对话 UI 确实不继续显示了,但 Go 进程的 CPU 占用迟迟不降。后来查明原因:前端 Wails 绑定方法调用ChatStream时,前端虽然不再等待返回值,但后端 HTTP 请求如果没有被真正取消,协程就一直在等响应。

解决方案就是前面提到的cancelFuncs设计——在前端调用ChatStream时,把当前会话对应的 cancel 函数存下来,点击取消时主动调用。这是所有流式接口方案里最容易踩的坑,务必在一开始就设计好取消链路。

7.2 踩坑:WebView2 缓存导致更新不生效

有一段时间我给应用改了前端样式,发布新版本后,用户反馈界面还是旧版。排查下来发现是 WebView2 的 HTTP 缓存把旧的 JS/CSS 资源缓存了。因为 Wails 的AssetServer默认按标准 HTTP 缓存策略处理资源,版本更新后资源 URL 没变化时,WebView2 就会继续用本地缓存。

我的解决方式是用 Vite 的build.generatedAssets或文件哈希——每次构建时给资源文件名加上内容哈希,这样更新版本后资源 URL 随之变化,缓存就自然失效了。在vite.config.ts中配置:

build: { rollupOptions: { output: { entryFileNames: "assets/[name]-[hash].js", chunkFileNames: "assets/[name]-[hash].js", }, }, }

这样发布新版本时,文件名发生变化,基本可以杜绝缓存问题。

7.3 踩坑:并发请求时的上下文管理

Wails v2 中,OnStartup里拿到的context.Context是应用生命周期级的,适合存窗口上下文。但如果你在多个会话中并发触发流式请求,全局用一个 context 管理会导致"取消一个会话,全部会话都断了"。所以每个ChatStream请求都应该基于它自己的context.WithCancel,存放在结构体的并发安全 Map 中,不能复用全局 ctx。

7.4 性能优化:减少序列化与频繁 GC

日常使用中,对话历史较长时,Go 后端的 JSON 序列化开销不可忽视。我的优化主要做了两件事:一是对话记录批量写入 SQLite,而不是每收到一个 chunk 就写库;二是流式增量只传content字段,不要把完整的消息对象反复序列化。

此外,对于那些体积很大的 Markdown 渲染,我会在前端做"先纯文本、结束后再渲染"的策略(前面 5.2 提过),这既减少了前端 DOM 频繁更新的压力,也规避了解析过程中的闪烁。实测在 300 行以上的长文档场景中,这个策略能让滚动保持流畅。

7.5 最后再分享一个小技巧

如果要在发布时保留 Go 后端日志,建议在 Windows 上构建时使用-ldflags "-s -w"但不要把-H windowsgui加上,这样程序运行时会带一个控制台窗口,直接打印日志,排查线上问题非常方便。等确认稳定后,再用-H windowsgui去掉这个窗口,改造成纯 GUI 模式。这个技巧帮助我节省了很多分发后的问题定位时间。

整个项目做下来,我最直观的感受是:Wails v2 给我提供了一条比 Electron 轻量、比 Tauri 更易上手的桌面应用路径,而 Go 后端意外地适合做 AI 应用的业务逻辑层。如果你也要做 AI 桌面工具,可以试试我的这套"Go 后端 + Vue3 前端 + 事件流桥接"方案,希望能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询