1
0

fix: Windows 桌面应用打包问题修复

- 删除通用 desktop target,重命名 platform targets 为简短形式 (desktop-mac/win/linux)
- 构建产物文件名统一为 nex-{os}-{arch}[.exe] 格式
- Windows 托盘图标使用 .ico 格式(运行时按平台选择)
- Windows 原生对话框使用 user32.MessageBoxW 替代 msg * 命令
- 更新 README.md 和 package-macos.sh 中的引用
- 添加单元测试覆盖 MessageBoxW 封装和图标选择逻辑
- 同步更新 desktop-app spec 规范文档
This commit is contained in:
2026-04-22 23:20:39 +08:00
parent 15f08ee2ca
commit 64dc66afa6
12 changed files with 370 additions and 33 deletions

View File

@@ -3,7 +3,7 @@
backend-lint backend-deps backend-generate \
backend-migrate-up backend-migrate-down backend-migrate-status backend-migrate-create \
frontend-build frontend-dev frontend-test frontend-test-watch frontend-test-coverage frontend-test-e2e frontend-lint \
desktop desktop-darwin desktop-windows desktop-linux package-macos
desktop-mac desktop-win desktop-linux package-macos
# ============================================
# 后端
@@ -82,9 +82,6 @@ frontend-lint:
# 桌面应用
# ============================================
desktop: frontend-build-desktop embedfs-prepare
cd backend && CGO_ENABLED=1 go build -o ../build/nex ./cmd/desktop
frontend-build-desktop:
cd frontend && cp .env.desktop .env.production.local && bun install && bun run build && rm -f .env.production.local
@@ -93,12 +90,12 @@ embedfs-prepare:
cp -r assets embedfs/assets
cp -r frontend/dist embedfs/frontend-dist
desktop-darwin: frontend-build-desktop embedfs-prepare
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../build/nex-darwin-arm64 ./cmd/desktop
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../build/nex-darwin-amd64 ./cmd/desktop
desktop-mac: frontend-build-desktop embedfs-prepare
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../build/nex-mac-arm64 ./cmd/desktop
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../build/nex-mac-amd64 ./cmd/desktop
desktop-windows: frontend-build-desktop embedfs-prepare
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o ../build/nex-windows-amd64.exe ./cmd/desktop
desktop-win: frontend-build-desktop embedfs-prepare
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o ../build/nex-win-amd64.exe ./cmd/desktop
desktop-linux: frontend-build-desktop embedfs-prepare
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o ../build/nex-linux-amd64 ./cmd/desktop

View File

@@ -91,22 +91,19 @@ nex/
**构建桌面应用**
```bash
# 当前平台
make desktop
# macOS (arm64 + amd64)
make desktop-darwin
make desktop-mac
make package-macos # 打包为 .app
# Windows
make desktop-windows
make desktop-win
# Linux
make desktop-linux
```
**使用桌面应用**
- 双击启动应用macOS: Nex.appWindows: nex.exeLinux: nex
- 双击启动应用macOS: Nex.appWindows: nex-win-amd64.exeLinux: nex-linux-amd64
- 系统托盘图标出现,浏览器自动打开管理界面
- 点击托盘图标显示菜单,可打开管理界面或退出
- 关闭浏览器后服务继续运行,可通过托盘重新打开

View File

@@ -0,0 +1,33 @@
package main
import (
"runtime"
"testing"
"nex/embedfs"
)
func TestIconSelection_Windows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("图标格式选择测试仅在 Windows 上运行")
}
if err := testIconLoad("assets/icon.ico"); err != nil {
t.Fatalf("Windows 应加载 .ico 文件: %v", err)
}
}
func TestIconSelection_NonWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("图标格式选择测试在非 Windows 平台运行")
}
if err := testIconLoad("assets/icon.png"); err != nil {
t.Fatalf("非 Windows 平台应加载 .png 文件: %v", err)
}
}
func testIconLoad(path string) error {
_, err := embedfs.Assets.ReadFile(path)
return err
}

View File

@@ -12,7 +12,9 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"unsafe"
"github.com/gin-gonic/gin"
"github.com/getlantern/systray"
@@ -329,7 +331,13 @@ func setupStaticFiles(r *gin.Engine) {
func setupSystray(port int) {
systray.Run(func() {
icon, err := embedfs.Assets.ReadFile("assets/icon.png")
var icon []byte
var err error
if runtime.GOOS == "windows" {
icon, err = embedfs.Assets.ReadFile("assets/icon.ico")
} else {
icon, err = embedfs.Assets.ReadFile("assets/icon.png")
}
if err != nil {
zapLogger.Error("无法加载托盘图标", zap.String("error", err.Error()))
}
@@ -446,7 +454,7 @@ func showError(title, message string) {
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`, message, title)
exec.Command("osascript", "-e", script).Run()
case "windows":
exec.Command("msg", "*", message).Run()
messageBox(title, message, MB_ICONERROR)
case "linux":
exec.Command("zenity", "--error", fmt.Sprintf("--title=%s", title), fmt.Sprintf("--text=%s", message)).Run()
}
@@ -459,8 +467,29 @@ func showAbout() {
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "关于 Nex Gateway"`, message)
exec.Command("osascript", "-e", script).Run()
case "windows":
exec.Command("msg", "*", message).Run()
messageBox("关于 Nex Gateway", message, MB_ICONINFORMATION)
case "linux":
exec.Command("zenity", "--info", "--title=关于 Nex Gateway", fmt.Sprintf("--text=%s", message)).Run()
}
}
const (
MB_ICONERROR = 0x10
MB_ICONINFORMATION = 0x40
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
procMessageBoxW = user32.NewProc("MessageBoxW")
)
func messageBox(title, message string, flags uint) {
titlePtr, _ := syscall.UTF16PtrFromString(title)
messagePtr, _ := syscall.UTF16PtrFromString(message)
procMessageBoxW.Call(
0,
uintptr(unsafe.Pointer(messagePtr)),
uintptr(unsafe.Pointer(titlePtr)),
uintptr(flags),
)
}

View File

@@ -0,0 +1,30 @@
package main
import (
"runtime"
"testing"
)
func TestMessageBoxW_WindowsOnly(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("MessageBoxW 仅在 Windows 上测试")
}
messageBox("测试标题", "测试消息", MB_ICONINFORMATION)
}
func TestShowError_WindowsBranch(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows 原生对话框测试仅在 Windows 上运行")
}
showError("测试错误", "这是一条测试错误消息")
}
func TestShowAbout_WindowsBranch(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows 原生对话框测试仅在 Windows 上运行")
}
showAbout()
}

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-22

View File

@@ -0,0 +1,81 @@
## Context
Nex 桌面应用是一个将后端服务Go/Gin与前端静态资源embed.FS打包为单一可执行文件的跨平台应用。当前 Windows 构建存在三类问题:
1. **通用 `desktop` target 无平台感知**:输出 `build/nex``.exe` 后缀,且缺少 `-H=windowsgui` linker flag 导致控制台窗口闪现
2. **系统托盘图标加载失败**`getlantern/systray` 在 Windows 上期望 ICO 格式,代码传入了 64x64 的 PNG
3. **`showError`/`showAbout` 使用 `msg *`**Windows Home 版本可能不可用,配合 `-H=windowsgui` 后行为不可预测,且不支持自定义标题栏
项目已有 `assets/icon.ico`256x256但代码未使用。
## Goals / Non-Goals
**Goals:**
- Windows 构建产物可直接双击运行(.exe 后缀、无控制台窗口)
- 系统托盘图标在所有平台上正确加载
- Windows 上使用原生 `MessageBoxW` 对话框替代 `msg *`
- Makefile target 命名简洁统一
**Non-Goals:**
- 不引入新的第三方依赖
- 不改变 macOS/Linux 上的现有行为
- 不涉及应用签名或代码公证(属于发布流程)
- 不重构整体打包架构
## Decisions
### 1. 删除通用 `desktop` target重命名平台 target
**决策**:删除 `desktop` target`desktop-darwin`/`desktop-windows`/`desktop-linux` 重命名为 `desktop-mac`/`desktop-win`/`desktop-linux`
**理由**:通用 target 在跨平台构建时必然需要条件判断,增加复杂度。按平台分离更明确,且项目已有先例。短命名 `win`/`mac`/`linux` 更简洁。
**产物命名统一**`nex-{os}-{arch}[.exe]`
- `nex-mac-arm64``nex-mac-amd64`
- `nex-win-amd64.exe`
- `nex-linux-amd64`
### 2. 托盘图标运行时按平台选择格式
**决策**:在 `setupSystray` 中根据 `runtime.GOOS` 选择图标文件:
- Windows加载 `assets/icon.ico`256x256 ICO
- 其他:加载 `assets/icon.png`PNG
**备选方案**
- ~~Build tags + 编译时选择~~:增加文件数,维护成本高
- ~~所有平台统一用 ICO~~Linux/macOS 的 systray 实现对 ICO 支持不一致
**理由**:运行时判断最简单,两个文件都已通过 `embedfs.Assets``assets/*`)嵌入,零额外成本。
### 3. Windows 原生对话框使用 `user32.MessageBoxW`
**决策**:通过 `syscall` 调用 `user32.dll``MessageBoxW`,替换 `msg *`
**实现方式**
```go
var (
user32 = syscall.NewLazyDLL("user32.dll")
procMessageBoxW = user32.NewProc("MessageBoxW")
)
func messageBox(title, message string) {
procMessageBoxW.Call(0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(message))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))), 0x10)
}
```
**备选方案**
- ~~继续用 `msg *`~~:不解决 Home 版不可用、标题栏不支持的问题
- ~~`rundll32` 调用~~:同样不可靠
- ~~引入 `lxn/walk` 等 GUI 库~~:引入重依赖,过度
**理由**`MessageBoxW` 是 Windows 原生 API所有版本都有`-H=windowsgui` 完美兼容,支持标题栏和图标类型,零依赖。使用 `syscall`(非 `unsafe` 外部依赖)即可。
### 4. `showError`/`showAbout` 统一用平台 switch
**决策**:保持现有的 `switch runtime.GOOS` 结构,仅替换 Windows 分支实现。macOSosascript和 Linuxzenity不变。
## Risks / Trade-offs
- **[syscall 跨架构]** `MessageBoxW``syscall.NewLazyDLL` 仅在 Windows 上有效 → 使用 `runtime.GOOS` 守卫,非 Windows 不会执行该路径,编译时通过 build 文件或运行时判断确保不触发
- **[ICO 嵌入体积]** `icon.ico` 270KB已在 `embedfs` 中,不增加新体积 → 无风险
- **[Makefile 兼容性]** 删除 `desktop` target 后CI/本地脚本如果引用它需更新 → 需检查是否有外部引用

View File

@@ -0,0 +1,28 @@
## Why
Windows 桌面应用存在三个影响用户体验的问题:构建产物无 `.exe` 后缀无法双击运行、运行时弹出控制台窗口、系统托盘图标加载失败。此外 `showError`/`showAbout` 在 Windows 上使用 `msg *` 命令不可靠。这些问题导致应用在 Windows 上不够专业,需要统一修复。
## What Changes
- 删除通用 `desktop` Makefile target仅保留按平台分离的 target
- Makefile target 重命名为简短形式:`desktop-win``desktop-mac``desktop-linux`
- 构建产物文件名统一为 `nex-{os}-{arch}[.exe]` 格式
- 系统托盘图标在 Windows 上使用 `.ico` 格式(运行时 `runtime.GOOS` 判断)
- Windows `showError`/`showAbout` 改用 `user32.dll``MessageBoxW` 原生对话框
- 同步更新已有 `desktop-app` spec 中的构建产物命名和图标格式要求
## Capabilities
### New Capabilities
### Modified Capabilities
- `desktop-app`: 构建产物命名规范变更(`nex-{os}-{arch}`Windows 托盘图标需使用 `.ico` 格式Windows 原生对话框替代 `msg *` 命令
## Impact
- `Makefile`:删除 `desktop` target重命名其余三个 target 和产物文件名
- `backend/cmd/desktop/main.go`:修改 `setupSystray` 图标加载逻辑、`showError`/`showAbout` Windows 实现
- `openspec/specs/desktop-app/spec.md`:更新构建产物命名和 Windows 图标格式要求

View File

@@ -0,0 +1,87 @@
## MODIFIED Requirements
### Requirement: 系统托盘
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。图标格式 SHALL 根据平台自动选择。
#### Scenario: 托盘图标显示
- **WHEN** 桌面应用启动成功
- **THEN** 系统根据平台加载正确的图标格式
- **AND** 在 Windows 上加载 ICO 格式图标(`assets/icon.ico`
- **AND** 在 macOS 和 Linux 上加载 PNG 格式图标(`assets/icon.png`
- **AND** 托盘图标 tooltip 显示"AI Gateway"
#### Scenario: 托盘菜单显示
- **WHEN** 用户点击托盘图标(左键或右键)
- **THEN** 显示托盘菜单
- **AND** 菜单包含"打开管理界面"选项
- **AND** 菜单包含"状态: 运行中"选项(禁用状态)
- **AND** 菜单包含"端口: 9826"选项(禁用状态)
- **AND** 菜单包含"关于"选项
- **AND** 菜单包含"退出"选项
#### Scenario: 打开管理界面
- **WHEN** 用户点击托盘菜单"打开管理界面"
- **THEN** 系统在浏览器中打开 `http://localhost:9826`
#### Scenario: 浏览器打开失败
- **WHEN** 系统无法打开浏览器(浏览器未安装等)
- **THEN** 托盘菜单仍可正常使用
- **AND** 用户可手动访问 `http://localhost:9826`
#### Scenario: 退出应用
- **WHEN** 用户点击托盘菜单"退出"
- **THEN** 系统优雅关闭后端服务
- **AND** 托盘图标消失
- **AND** 应用进程退出
### Requirement: 跨平台构建
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,产物文件名 SHALL 使用 `nex-{os}-{arch}[.exe]` 格式。
#### Scenario: macOS 构建
- **WHEN** 执行 `desktop-mac` 构建命令
- **THEN** 生成 `nex-mac-arm64``nex-mac-amd64` 可执行文件
- **AND** 可打包为 `.app` bundle
#### Scenario: Windows 构建
- **WHEN** 执行 `desktop-win` 构建命令
- **THEN** 生成 `nex-win-amd64.exe` 可执行文件
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
#### Scenario: Linux 构建
- **WHEN** 执行 `desktop-linux` 构建命令
- **THEN** 生成 `nex-linux-amd64` 可执行文件
### Requirement: 关于对话框
系统 SHALL 提供关于对话框显示应用信息。在 Windows 上 SHALL 使用 `user32.dll``MessageBoxW` API 实现。
#### Scenario: 显示关于
- **WHEN** 用户点击托盘菜单"关于"
- **THEN** 显示对话框包含应用名称、项目链接
- **AND** 在 Windows 上使用 `MessageBoxW` 原生对话框实现
## ADDED Requirements
### Requirement: Windows 原生对话框
系统 SHALL 在 Windows 上使用 `user32.dll``MessageBoxW` API 显示错误和关于对话框,替代 `msg *` 命令。
#### Scenario: 错误提示对话框
- **WHEN** 应用在 Windows 上遇到启动错误(端口占用、配置加载失败等)
- **THEN** 使用 `MessageBoxW` 显示模态对话框
- **AND** 对话框标题栏显示应用名称
- **AND** 对话框包含错误描述文本
- **AND** 对话框显示错误图标MB_ICONERROR
#### Scenario: 关于对话框
- **WHEN** 用户在 Windows 上点击托盘菜单"关于"
- **THEN** 使用 `MessageBoxW` 显示模态对话框
- **AND** 对话框标题栏显示"关于 Nex Gateway"
- **AND** 对话框包含应用信息文本
- **AND** 对话框显示信息图标MB_ICONINFORMATION
#### Scenario: 非 Windows 平台不受影响
- **WHEN** 应用运行在 macOS 或 Linux 上
- **THEN** 错误和关于对话框仍使用平台原有实现osascript / zenity

View File

@@ -0,0 +1,28 @@
## 1. Makefile 重构
- [x] 1.1 删除通用 `desktop` target 及其相关 `.PHONY` 声明
- [x] 1.2 将 `desktop-darwin` 重命名为 `desktop-mac`,产物文件名改为 `nex-mac-arm64``nex-mac-amd64`
- [x] 1.3 将 `desktop-windows` 重命名为 `desktop-win`,产物文件名改为 `nex-win-amd64.exe`
- [x] 1.4 将 `desktop-linux` 产物文件名改为 `nex-linux-amd64`
- [x] 1.5 更新 `.PHONY` 声明和 `all` target如引用了旧名称
## 2. Windows 原生对话框
- [x] 2.1 在 `backend/cmd/desktop/main.go` 中添加 Windows 平台的 `user32.MessageBoxW` 调用封装(`syscall.NewLazyDLL` + `syscall.StringToUTF16Ptr`),在 `showError`/`showAbout` 的 Windows `runtime.GOOS` 分支内直接调用
- [x] 2.2 替换 `showError` 函数的 Windows 分支,使用 `MessageBoxW` 替代 `msg *`
- [x] 2.3 替换 `showAbout` 函数的 Windows 分支,使用 `MessageBoxW` 替代 `msg *`
## 3. 系统托盘图标修复
- [x] 3.1 修改 `setupSystray` 函数中的图标加载逻辑,根据 `runtime.GOOS` 在 Windows 上加载 `assets/icon.ico`,其他平台加载 `assets/icon.png`
## 4. 文档和脚本更新
- [x] 4.1 更新 `README.md` 中的构建命令引用(`desktop-darwin``desktop-mac``desktop-windows``desktop-win``desktop-linux` 保持不变或改为 `desktop-linux`
- [x] 4.2 更新 `scripts/build/package-macos.sh` 中对 `desktop-darwin` 的引用
## 5. 测试验证
- [x] 5.1 为 `showError`/`showAbout` 的 Windows `MessageBoxW` 封装编写单元测试(验证参数传递和调用正确性)
- [x] 5.2 为图标加载的平台选择逻辑编写单元测试(验证 Windows 选 `.ico`,其他选 `.png`
- [x] 5.3 运行 `make desktop-win` 在 Windows 上验证:产物有 `.exe` 后缀、双击无控制台窗口、托盘图标正常显示、错误对话框使用原生样式

View File

@@ -31,11 +31,13 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
### Requirement: 系统托盘
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。图标格式 SHALL 根据平台自动选择。
#### Scenario: 托盘图标显示
- **WHEN** 桌面应用启动成功
- **THEN** 系统托盘区域显示应用图标
- **THEN** 系统根据平台加载正确的图标格式
- **AND** 在 Windows 上加载 ICO 格式图标(`assets/icon.ico`
- **AND** 在 macOS 和 Linux 上加载 PNG 格式图标(`assets/icon.png`
- **AND** 托盘图标 tooltip 显示"AI Gateway"
#### Scenario: 托盘菜单显示
@@ -93,20 +95,20 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
### Requirement: 跨平台构建
系统 SHALL 支持跨平台构建和打包。
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,产物文件名 SHALL 使用 `nex-{os}-{arch}[.exe]` 格式。
#### Scenario: macOS 构建
- **WHEN** 执行 macOS 构建命令
- **THEN** 生成 `nex-darwin-arm64``nex-darwin-amd64` 可执行文件
- **WHEN** 执行 `desktop-mac` 构建命令
- **THEN** 生成 `nex-mac-arm64``nex-mac-amd64` 可执行文件
- **AND** 可打包为 `.app` bundle
#### Scenario: Windows 构建
- **WHEN** 执行 Windows 构建命令
- **THEN** 生成 `nex-windows-amd64.exe` 可执行文件
- **AND** 运行时不显示控制台窗口
- **WHEN** 执行 `desktop-win` 构建命令
- **THEN** 生成 `nex-win-amd64.exe` 可执行文件
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
#### Scenario: Linux 构建
- **WHEN** 执行 Linux 构建命令
- **WHEN** 执行 `desktop-linux` 构建命令
- **THEN** 生成 `nex-linux-amd64` 可执行文件
### Requirement: macOS .app 打包
@@ -123,8 +125,31 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
### Requirement: 关于对话框
系统 SHALL 提供关于对话框显示应用信息。
系统 SHALL 提供关于对话框显示应用信息。在 Windows 上 SHALL 使用 `user32.dll``MessageBoxW` API 实现。
#### Scenario: 显示关于
- **WHEN** 用户点击托盘菜单"关于"
- **THEN** 显示对话框包含应用名称、项目链接
- **AND** 在 Windows 上使用 `MessageBoxW` 原生对话框实现
### Requirement: Windows 原生对话框
系统 SHALL 在 Windows 上使用 `user32.dll``MessageBoxW` API 显示错误和关于对话框,替代 `msg *` 命令。
#### Scenario: 错误提示对话框
- **WHEN** 应用在 Windows 上遇到启动错误(端口占用、配置加载失败等)
- **THEN** 使用 `MessageBoxW` 显示模态对话框
- **AND** 对话框标题栏显示应用名称
- **AND** 对话框包含错误描述文本
- **AND** 对话框显示错误图标MB_ICONERROR
#### Scenario: 关于对话框
- **WHEN** 用户在 Windows 上点击托盘菜单"关于"
- **THEN** 使用 `MessageBoxW` 显示模态对话框
- **AND** 对话框标题栏显示"关于 Nex Gateway"
- **AND** 对话框包含应用信息文本
- **AND** 对话框显示信息图标MB_ICONINFORMATION
#### Scenario: 非 Windows 平台不受影响
- **WHEN** 应用运行在 macOS 或 Linux 上
- **THEN** 错误和关于对话框仍使用平台原有实现osascript / zenity

View File

@@ -13,12 +13,12 @@ echo "打包 macOS .app..."
mkdir -p "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS"
mkdir -p "${BUILD_DIR}/${APP_NAME}.app/Contents/Resources"
if [ -f "${BUILD_DIR}/nex-darwin-arm64" ]; then
cp "${BUILD_DIR}/nex-darwin-arm64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex"
elif [ -f "${BUILD_DIR}/nex-darwin-amd64" ]; then
cp "${BUILD_DIR}/nex-darwin-amd64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex"
if [ -f "${BUILD_DIR}/nex-mac-arm64" ]; then
cp "${BUILD_DIR}/nex-mac-arm64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex"
elif [ -f "${BUILD_DIR}/nex-mac-amd64" ]; then
cp "${BUILD_DIR}/nex-mac-amd64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex"
else
echo "错误: 未找到 macOS 二进制文件,请先运行 make desktop-darwin"
echo "错误: 未找到 macOS 二进制文件,请先运行 make desktop-mac"
exit 1
fi