1
0

fix: 修复 macOS 桌面应用打包与元数据

将 macOS 桌面应用改为通用二进制并动态写入最低系统版本,避免 Intel Mac 无法启动。统一桌面应用名称与托盘展示,并补充测试确保相关行为稳定。
This commit is contained in:
2026-04-24 19:35:51 +08:00
parent b2e9dd8b7f
commit 4c62c071fb
8 changed files with 69 additions and 33 deletions

View File

@@ -172,41 +172,51 @@ desktop-build-mac: desktop-prepare-frontend desktop-prepare-embedfs
@echo "🍎 Building macOS..." @echo "🍎 Building macOS..."
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=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 cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../build/nex-mac-amd64 ./cmd/desktop
lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal
@echo "📦 Packaging macOS .app..." @echo "📦 Packaging macOS .app..."
mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources
cp build/nex-mac-arm64 build/Nex.app/Contents/MacOS/nex cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex
@if [ -f assets/AppIcon.icns ]; then \ @if [ -f assets/AppIcon.icns ]; then \
cp assets/AppIcon.icns build/Nex.app/Contents/Resources/; \ cp assets/AppIcon.icns build/Nex.app/Contents/Resources/; \
else \ else \
echo "⚠️ 未找到 assets/AppIcon.icns"; \ echo "⚠️ 未找到 assets/AppIcon.icns"; \
fi fi
@{ \ @MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \
if [ -z "$$MIN_MACOS_VERSION" ]; then \
echo "❌ 无法读取 macOS 最低系统版本"; \
exit 1; \
fi; \
{ \
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \ printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
'<plist version="1.0">' \ '<plist version="1.0">' \
'<dict>' \ '<dict>' \
' <key>CFBundleDevelopmentRegion</key>' \ ' <key>CFBundleDevelopmentRegion</key>' \
' <string>zh_CN</string>' \ ' <string>zh-Hans</string>' \
' <key>CFBundleExecutable</key>' \ ' <key>CFBundleExecutable</key>' \
' <string>nex</string>' \ ' <string>nex</string>' \
' <key>CFBundleIconFile</key>' \ ' <key>CFBundleIconFile</key>' \
' <string>AppIcon</string>' \ ' <string>AppIcon</string>' \
' <key>CFBundleIdentifier</key>' \ ' <key>CFBundleIdentifier</key>' \
' <string>io.nex.gateway</string>' \ ' <string>com.lanyuanxiaoyao.nex</string>' \
' <key>CFBundleInfoDictionaryVersion</key>' \ ' <key>CFBundleInfoDictionaryVersion</key>' \
' <string>6.0</string>' \ ' <string>6.0</string>' \
' <key>LSApplicationCategoryType</key>' \
' <string>public.app-category.developer-tools</string>' \
' <key>CFBundleName</key>' \ ' <key>CFBundleName</key>' \
' <string>Nex Gateway</string>' \ ' <string>Nex</string>' \
' <key>CFBundleDisplayName</key>' \ ' <key>CFBundleDisplayName</key>' \
' <string>Nex Gateway</string>' \ ' <string>Nex</string>' \
' <key>CFBundlePackageType</key>' \ ' <key>CFBundlePackageType</key>' \
' <string>APPL</string>' \ ' <string>APPL</string>' \
' <key>CFBundleShortVersionString</key>' \ ' <key>CFBundleShortVersionString</key>' \
' <string>1.0.0</string>' \ ' <string>1.0.0</string>' \
' <key>CFBundleVersion</key>' \ ' <key>CFBundleVersion</key>' \
' <string>1.0.0</string>' \ ' <string>1.0.0</string>' \
' <key>NSHumanReadableCopyright</key>' \
' <string>Copyright © 2026 Nex</string>' \
' <key>LSMinimumSystemVersion</key>' \ ' <key>LSMinimumSystemVersion</key>' \
' <string>10.13</string>' \ " <string>$$MIN_MACOS_VERSION</string>" \
' <key>LSUIElement</key>' \ ' <key>LSUIElement</key>' \
' <true/>' \ ' <true/>' \
' <key>NSHighResolutionCapable</key>' \ ' <key>NSHighResolutionCapable</key>' \

View File

@@ -19,9 +19,8 @@ func showError(title, message string) {
} }
func showAbout() { func showAbout() {
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway" script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`,
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "关于 Nex Gateway"`, escapeAppleScript(aboutMessage()), escapeAppleScript(appAboutTitle))
escapeAppleScript(message))
if err := exec.Command("osascript", "-e", script).Run(); err != nil { if err := exec.Command("osascript", "-e", script).Run(); err != nil {
dialogLogger().Warn("显示关于对话框失败", zap.Error(err)) dialogLogger().Warn("显示关于对话框失败", zap.Error(err))
} }

View File

@@ -67,21 +67,19 @@ func showError(title, message string) {
} }
func showAbout() { func showAbout() {
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway"
switch dialogTool { switch dialogTool {
case toolZenity: case toolZenity:
exec.Command("zenity", "--info", exec.Command("zenity", "--info",
"--title=关于 Nex Gateway", fmt.Sprintf("--title=%s", appAboutTitle),
fmt.Sprintf("--text=%s", message)).Run() fmt.Sprintf("--text=%s", aboutMessage())).Run()
case toolKdialog: case toolKdialog:
exec.Command("kdialog", "--msgbox", message, "--title", "关于 Nex Gateway").Run() exec.Command("kdialog", "--msgbox", aboutMessage(), "--title", appAboutTitle).Run()
case toolNotifySend: case toolNotifySend:
exec.Command("notify-send", "关于 Nex Gateway", message).Run() exec.Command("notify-send", appAboutTitle, aboutMessage()).Run()
case toolXmessage: case toolXmessage:
exec.Command("xmessage", "-center", exec.Command("xmessage", "-center",
fmt.Sprintf("关于 Nex Gateway: %s", message)).Run() fmt.Sprintf("%s: %s", appAboutTitle, aboutMessage())).Run()
default: default:
dialogLogger().Info("关于 Nex Gateway") dialogLogger().Info(appAboutTitle)
} }
} }

View File

@@ -22,8 +22,7 @@ func showError(title, message string) {
} }
func showAbout() { func showAbout() {
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway" messageBox(appAboutTitle, aboutMessage(), MB_ICONINFORMATION)
messageBox("关于 Nex Gateway", message, MB_ICONINFORMATION)
} }
func messageBox(title, message string, flags uint) { func messageBox(title, message string, flags uint) {

View File

@@ -49,7 +49,7 @@ func main() {
singleLock := NewSingletonLock(filepath.Join(os.TempDir(), "nex-gateway.lock")) singleLock := NewSingletonLock(filepath.Join(os.TempDir(), "nex-gateway.lock"))
if err := singleLock.Lock(); err != nil { if err := singleLock.Lock(); err != nil {
minimalLogger.Error("已有 Nex 实例运行") minimalLogger.Error("已有 Nex 实例运行")
showError("Nex Gateway", "已有 Nex 实例运行") showError(appName, "已有 Nex 实例运行")
os.Exit(1) os.Exit(1)
} }
defer func() { defer func() {
@@ -60,7 +60,7 @@ func main() {
if err := checkPortAvailable(port); err != nil { if err := checkPortAvailable(port); err != nil {
minimalLogger.Error("端口不可用", zap.Error(err)) minimalLogger.Error("端口不可用", zap.Error(err))
showError("Nex Gateway", err.Error()) showError(appName, err.Error())
return return
} }
@@ -278,8 +278,7 @@ func setupSystray(port int) {
zapLogger.Error("无法加载托盘图标", zap.Error(err)) zapLogger.Error("无法加载托盘图标", zap.Error(err))
} }
systray.SetIcon(icon) systray.SetIcon(icon)
systray.SetTitle("Nex Gateway") systray.SetTooltip(appTooltip)
systray.SetTooltip("AI Gateway")
mOpen := systray.AddMenuItem("打开管理界面", "在浏览器中打开") mOpen := systray.AddMenuItem("打开管理界面", "在浏览器中打开")
systray.AddSeparator() systray.AddSeparator()

View File

@@ -0,0 +1,13 @@
package main
const (
appName = "Nex"
appTooltip = appName
appAboutTitle = "关于 " + appName
appDescription = "AI Gateway - 统一的大模型 API 网关"
appWebsite = "https://github.com/nex/gateway"
)
func aboutMessage() string {
return appName + "\n\n" + appDescription + "\n\n" + appWebsite
}

View File

@@ -0,0 +1,24 @@
package main
import "testing"
func TestAboutMessage(t *testing.T) {
expected := "Nex\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway"
if got := aboutMessage(); got != expected {
t.Fatalf("aboutMessage() = %q, want %q", got, expected)
}
}
func TestDesktopMetadata(t *testing.T) {
if appName != "Nex" {
t.Fatalf("appName = %q, want %q", appName, "Nex")
}
if appTooltip != appName {
t.Fatalf("appTooltip = %q, want %q", appTooltip, appName)
}
if appAboutTitle != "关于 Nex" {
t.Fatalf("appAboutTitle = %q, want %q", appAboutTitle, "关于 Nex")
}
}

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"net" "net"
"net/http" "net/http"
"testing" "testing"
@@ -21,19 +22,12 @@ func TestCheckPortAvailable(t *testing.T) {
func TestCheckPortOccupied(t *testing.T) { func TestCheckPortOccupied(t *testing.T) {
port := 19827 port := 19827
listener, err := net.Listen("tcp", "127.0.0.1:19827") listener, err := net.Listen("tcp", ":19827")
if err != nil { if err != nil {
t.Fatalf("无法启动测试服务器: %v", err) t.Fatalf("无法启动测试服务器: %v", err)
} }
defer listener.Close() defer listener.Close()
go func() {
conn, err := listener.Accept()
if err == nil {
conn.Close()
}
}()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
err = checkPortAvailable(port) err = checkPortAvailable(port)
@@ -56,7 +50,7 @@ func TestCheckPortAvailableAfterClose(t *testing.T) {
defer server.Close() defer server.Close()
go func() { go func() {
err := server.Serve(listener) err := server.Serve(listener)
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed && !errors.Is(err, net.ErrClosed) {
t.Errorf("serve failed: %v", err) t.Errorf("serve failed: %v", err)
} }
}() }()