fix: 修复 macOS 桌面应用打包与元数据
将 macOS 桌面应用改为通用二进制并动态写入最低系统版本,避免 Intel Mac 无法启动。统一桌面应用名称与托盘展示,并补充测试确保相关行为稳定。
This commit is contained in:
24
Makefile
24
Makefile
@@ -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>' \
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
13
backend/cmd/desktop/metadata.go
Normal file
13
backend/cmd/desktop/metadata.go
Normal 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
|
||||||
|
}
|
||||||
24
backend/cmd/desktop/metadata_test.go
Normal file
24
backend/cmd/desktop/metadata_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
Reference in New Issue
Block a user