diff --git a/Makefile b/Makefile index 1ac985b..3716705 100644 --- a/Makefile +++ b/Makefile @@ -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' '' \ '' \ '' \ '' \ ' CFBundleDevelopmentRegion' \ - ' zh_CN' \ + ' zh-Hans' \ ' CFBundleExecutable' \ ' nex' \ ' CFBundleIconFile' \ ' AppIcon' \ ' CFBundleIdentifier' \ - ' io.nex.gateway' \ + ' com.lanyuanxiaoyao.nex' \ ' CFBundleInfoDictionaryVersion' \ ' 6.0' \ + ' LSApplicationCategoryType' \ + ' public.app-category.developer-tools' \ ' CFBundleName' \ - ' Nex Gateway' \ + ' Nex' \ ' CFBundleDisplayName' \ - ' Nex Gateway' \ + ' Nex' \ ' CFBundlePackageType' \ ' APPL' \ ' CFBundleShortVersionString' \ ' 1.0.0' \ ' CFBundleVersion' \ ' 1.0.0' \ + ' NSHumanReadableCopyright' \ + ' Copyright © 2026 Nex' \ ' LSMinimumSystemVersion' \ - ' 10.13' \ + " $$MIN_MACOS_VERSION" \ ' LSUIElement' \ ' ' \ ' NSHighResolutionCapable' \ diff --git a/backend/cmd/desktop/dialog_darwin.go b/backend/cmd/desktop/dialog_darwin.go index 9cfe217..baa7fc1 100644 --- a/backend/cmd/desktop/dialog_darwin.go +++ b/backend/cmd/desktop/dialog_darwin.go @@ -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)) } diff --git a/backend/cmd/desktop/dialog_linux.go b/backend/cmd/desktop/dialog_linux.go index 3bf7159..ee678d4 100644 --- a/backend/cmd/desktop/dialog_linux.go +++ b/backend/cmd/desktop/dialog_linux.go @@ -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) } } diff --git a/backend/cmd/desktop/dialog_windows.go b/backend/cmd/desktop/dialog_windows.go index 0ee461d..f95b915 100644 --- a/backend/cmd/desktop/dialog_windows.go +++ b/backend/cmd/desktop/dialog_windows.go @@ -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) { diff --git a/backend/cmd/desktop/main.go b/backend/cmd/desktop/main.go index 5dfdc96..d2a3545 100644 --- a/backend/cmd/desktop/main.go +++ b/backend/cmd/desktop/main.go @@ -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() diff --git a/backend/cmd/desktop/metadata.go b/backend/cmd/desktop/metadata.go new file mode 100644 index 0000000..100cc66 --- /dev/null +++ b/backend/cmd/desktop/metadata.go @@ -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 +} diff --git a/backend/cmd/desktop/metadata_test.go b/backend/cmd/desktop/metadata_test.go new file mode 100644 index 0000000..8827705 --- /dev/null +++ b/backend/cmd/desktop/metadata_test.go @@ -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") + } +} diff --git a/backend/cmd/desktop/port_test.go b/backend/cmd/desktop/port_test.go index 4d65a8e..6116a30 100644 --- a/backend/cmd/desktop/port_test.go +++ b/backend/cmd/desktop/port_test.go @@ -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) } }()