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..."
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
lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal
@echo "📦 Packaging macOS .app..."
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 \
cp assets/AppIcon.icns build/Nex.app/Contents/Resources/; \
else \
echo "⚠️ 未找到 assets/AppIcon.icns"; \
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"?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
'<plist version="1.0">' \
'<dict>' \
' <key>CFBundleDevelopmentRegion</key>' \
' <string>zh_CN</string>' \
' <string>zh-Hans</string>' \
' <key>CFBundleExecutable</key>' \
' <string>nex</string>' \
' <key>CFBundleIconFile</key>' \
' <string>AppIcon</string>' \
' <key>CFBundleIdentifier</key>' \
' <string>io.nex.gateway</string>' \
' <string>com.lanyuanxiaoyao.nex</string>' \
' <key>CFBundleInfoDictionaryVersion</key>' \
' <string>6.0</string>' \
' <key>LSApplicationCategoryType</key>' \
' <string>public.app-category.developer-tools</string>' \
' <key>CFBundleName</key>' \
' <string>Nex Gateway</string>' \
' <string>Nex</string>' \
' <key>CFBundleDisplayName</key>' \
' <string>Nex Gateway</string>' \
' <string>Nex</string>' \
' <key>CFBundlePackageType</key>' \
' <string>APPL</string>' \
' <key>CFBundleShortVersionString</key>' \
' <string>1.0.0</string>' \
' <key>CFBundleVersion</key>' \
' <string>1.0.0</string>' \
' <key>NSHumanReadableCopyright</key>' \
' <string>Copyright © 2026 Nex</string>' \
' <key>LSMinimumSystemVersion</key>' \
' <string>10.13</string>' \
" <string>$$MIN_MACOS_VERSION</string>" \
' <key>LSUIElement</key>' \
' <true/>' \
' <key>NSHighResolutionCapable</key>' \

View File

@@ -19,9 +19,8 @@ func showError(title, message string) {
}
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 "关于 Nex Gateway"`,
escapeAppleScript(message))
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`,
escapeAppleScript(aboutMessage()), escapeAppleScript(appAboutTitle))
if err := exec.Command("osascript", "-e", script).Run(); err != nil {
dialogLogger().Warn("显示关于对话框失败", zap.Error(err))
}

View File

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

View File

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

View File

@@ -49,7 +49,7 @@ func main() {
singleLock := NewSingletonLock(filepath.Join(os.TempDir(), "nex-gateway.lock"))
if err := singleLock.Lock(); err != nil {
minimalLogger.Error("已有 Nex 实例运行")
showError("Nex Gateway", "已有 Nex 实例运行")
showError(appName, "已有 Nex 实例运行")
os.Exit(1)
}
defer func() {
@@ -60,7 +60,7 @@ func main() {
if err := checkPortAvailable(port); err != nil {
minimalLogger.Error("端口不可用", zap.Error(err))
showError("Nex Gateway", err.Error())
showError(appName, err.Error())
return
}
@@ -278,8 +278,7 @@ func setupSystray(port int) {
zapLogger.Error("无法加载托盘图标", zap.Error(err))
}
systray.SetIcon(icon)
systray.SetTitle("Nex Gateway")
systray.SetTooltip("AI Gateway")
systray.SetTooltip(appTooltip)
mOpen := systray.AddMenuItem("打开管理界面", "在浏览器中打开")
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
import (
"errors"
"net"
"net/http"
"testing"
@@ -21,19 +22,12 @@ func TestCheckPortAvailable(t *testing.T) {
func TestCheckPortOccupied(t *testing.T) {
port := 19827
listener, err := net.Listen("tcp", "127.0.0.1:19827")
listener, err := net.Listen("tcp", ":19827")
if err != nil {
t.Fatalf("无法启动测试服务器: %v", err)
}
defer listener.Close()
go func() {
conn, err := listener.Accept()
if err == nil {
conn.Close()
}
}()
time.Sleep(100 * time.Millisecond)
err = checkPortAvailable(port)
@@ -56,7 +50,7 @@ func TestCheckPortAvailableAfterClose(t *testing.T) {
defer server.Close()
go func() {
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)
}
}()