1
0

3 Commits

Author SHA1 Message Date
4c62c071fb fix: 修复 macOS 桌面应用打包与元数据
将 macOS 桌面应用改为通用二进制并动态写入最低系统版本,避免 Intel Mac 无法启动。统一桌面应用名称与托盘展示,并补充测试确保相关行为稳定。
2026-04-24 19:35:51 +08:00
b2e9dd8b7f refactor: 合并 macOS 桌面打包流程
将 macOS .app 打包直接并入 desktop-build-mac,减少重复的桌面构建入口。\n\n同时移除未实现或已废弃的 desktop-package-* 命令和独立打包脚本,降低维护成本。
2026-04-24 19:02:21 +08:00
d143c5f3df fix: 补齐前端生成物忽略并消除构建告警
统一 Git、ESLint、Prettier 对测试和构建生成物的忽略规则,避免本地产物导致 frontend-build 失败。

补齐表单 effect 依赖,移除无关告警,让前端构建链路恢复稳定。
2026-04-24 18:53:53 +08:00
15 changed files with 121 additions and 117 deletions

View File

@@ -5,7 +5,7 @@
test-mysql-up test-mysql-down test-mysql test-mysql-quick \
frontend-build frontend-dev frontend-test frontend-test-watch frontend-test-coverage frontend-test-e2e frontend-lint frontend-clean \
desktop-build desktop-build-mac desktop-build-win desktop-build-linux \
desktop-dev desktop-test desktop-package-mac desktop-package-win desktop-package-linux desktop-clean \
desktop-dev desktop-test desktop-clean \
desktop-prepare-frontend desktop-prepare-embedfs
# ============================================
@@ -147,7 +147,7 @@ frontend-lint: frontend-install
cd frontend && bun run lint
frontend-clean:
rm -rf frontend/dist frontend/.next frontend/node_modules
rm -rf frontend/dist frontend/.next frontend/node_modules frontend/coverage frontend/playwright-report frontend/test-results frontend/tsconfig.tsbuildinfo
# ============================================
# 桌面应用
@@ -172,6 +172,60 @@ 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-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-Hans</string>' \
' <key>CFBundleExecutable</key>' \
' <string>nex</string>' \
' <key>CFBundleIconFile</key>' \
' <string>AppIcon</string>' \
' <key>CFBundleIdentifier</key>' \
' <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</string>' \
' <key>CFBundleDisplayName</key>' \
' <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>$$MIN_MACOS_VERSION</string>" \
' <key>LSUIElement</key>' \
' <true/>' \
' <key>NSHighResolutionCapable</key>' \
' <true/>' \
'</dict>' \
'</plist>'; \
} > build/Nex.app/Contents/Info.plist
chmod +x build/Nex.app/Contents/MacOS/nex
@echo "✅ macOS app packaged: build/Nex.app"
desktop-build-win: desktop-prepare-frontend desktop-prepare-embedfs
@echo "🪟 Building Windows..."
@@ -188,15 +242,6 @@ desktop-dev: desktop-prepare-frontend desktop-prepare-embedfs
desktop-test:
cd backend && go test ./cmd/desktop/... -v
desktop-package-mac:
./scripts/build/package-macos.sh
desktop-package-win:
@echo "⚠️ Windows packaging not implemented yet"
desktop-package-linux:
@echo "⚠️ Linux packaging not implemented yet"
desktop-clean:
rm -rf build/ embedfs/assets embedfs/frontend-dist

View File

@@ -39,10 +39,6 @@ nex/
│ ├── AppIcon.icns # macOS 应用图标
│ └── icon.ico # Windows 应用图标
├── scripts/ # 构建脚本
│ └── build/
│ └── package-macos.sh # macOS .app 打包脚本
└── README.md # 本文件
```
@@ -105,9 +101,8 @@ JSON: {"level":"info","logger":"handler.proxy","msg":"处理请求","method":
**构建桌面应用**
```bash
# macOS (arm64 + amd64)
# macOS (arm64 + amd64,并打包为 .app)
make desktop-build-mac
make desktop-package-mac # 打包为 .app
# Windows
make desktop-build-win

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)
}
}()

4
frontend/.gitignore vendored
View File

@@ -10,6 +10,10 @@ lerna-debug.log*
node_modules
dist
dist-ssr
coverage
playwright-report
test-results
*.tsbuildinfo
*.local
# Editor directories and files

View File

@@ -1,6 +1,8 @@
node_modules
dist
dist-ssr
playwright-report
test-results
bun.lock
package-lock.json
yarn.lock
@@ -8,6 +10,8 @@ pnpm-lock.yaml
.env.*
*.local
coverage
*.tsbuildinfo
.DS_Store
**/*.snap
**/__snapshots__/**
*.svg

View File

@@ -9,7 +9,7 @@ import localRules from './eslint-rules/index.js'
import eslintConfigPrettier from 'eslint-config-prettier'
export default tseslint.config(
{ ignores: ['dist'] },
{ ignores: ['dist', 'coverage', 'playwright-report', 'test-results', '*.tsbuildinfo'] },
...tanstackQuery.configs['flat/recommended'],
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],

View File

@@ -23,18 +23,16 @@ export function ModelForm({ open, model, providerId, providers, onSave, onCancel
const [form] = Form.useForm()
const isEdit = !!model
// 当弹窗打开或model变化时设置表单值
// 当弹窗打开或 model 变化时,同步表单初始值。
useEffect(() => {
if (open && form) {
if (model) {
// 编辑模式:设置现有值
form.setFieldsValue({
providerId: model.providerId,
modelName: model.modelName,
enabled: model.enabled,
})
} else {
// 新增模式重置表单并设置默认providerId
form.reset()
form.setFieldsValue({
providerId,
@@ -42,7 +40,7 @@ export function ModelForm({ open, model, providerId, providers, onSave, onCancel
})
}
}
}, [open, model, providerId]) // 移除form依赖避免循环
}, [form, open, model, providerId])
const handleSubmit = (context: SubmitContext) => {
if (context.validateResult === true && form) {

View File

@@ -40,7 +40,7 @@ export function ProviderForm({ open, provider, onSave, onCancel, loading }: Prov
form.setFieldsValue({ enabled: true, protocol: 'openai' })
}
}
}, [open, provider])
}, [form, open, provider])
const handleSubmit = (context: SubmitContext) => {
if (context.validateResult === true && form) {

View File

@@ -1,68 +0,0 @@
#!/bin/bash
VERSION="1.0.0"
APP_NAME="Nex"
BUNDLE_ID="io.nex.gateway"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
BUILD_DIR="${PROJECT_ROOT}/build"
ASSETS_DIR="${PROJECT_ROOT}/assets"
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-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-build-mac"
exit 1
fi
if [ -f "${ASSETS_DIR}/AppIcon.icns" ]; then
cp "${ASSETS_DIR}/AppIcon.icns" "${BUILD_DIR}/${APP_NAME}.app/Contents/Resources/"
else
echo "警告: 未找到 AppIcon.icns"
fi
cat > "${BUILD_DIR}/${APP_NAME}.app/Contents/Info.plist" << EOF
<?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>
<key>CFBundleExecutable</key>
<string>nex</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${APP_NAME} Gateway</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME} Gateway</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>LSUIElement</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
EOF
chmod +x "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex"
echo "打包完成: ${BUILD_DIR}/${APP_NAME}.app"