Compare commits
3 Commits
4eebdfb8db
...
4c62c071fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c62c071fb | |||
| b2e9dd8b7f | |||
| d143c5f3df |
67
Makefile
67
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
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
|
||||
|
||||
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
4
frontend/.gitignore
vendored
@@ -10,6 +10,10 @@ lerna-debug.log*
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
*.tsbuildinfo
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user