From 0b05e08705805f40adb4b33a958936ac6d3d41e2 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 22 Apr 2026 19:27:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=A1=8C=E9=9D=A2?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 desktop 应用入口,将后端与前端打包为单一可执行文件 - 集成系统托盘功能(getlantern/systray) - 支持单实例锁和端口冲突检测 - 启动时自动打开浏览器显示管理界面 - 新增 embedfs 模块嵌入静态资源 - 新增跨平台构建脚本(macOS/Windows/Linux) - 新增 macOS .app 打包脚本 - 统一 Makefile,移除 backend/Makefile - 更新 README 添加桌面应用使用说明 --- .gitignore | 7 +- Makefile | 115 +++++++ README.md | 93 ++++-- assets/AppIcon.icns | Bin 0 -> 91424 bytes assets/README.md | 64 ++++ assets/icon.ico | Bin 0 -> 270398 bytes assets/icon.png | Bin 0 -> 2019 bytes assets/icon.svg | 13 + backend/Makefile | 57 ---- backend/cmd/desktop/main.go | 456 ++++++++++++++++++++++++++ backend/cmd/desktop/port_test.go | 69 ++++ backend/cmd/desktop/singleton_test.go | 39 +++ backend/cmd/desktop/static_test.go | 123 +++++++ backend/go.mod | 12 + backend/go.sum | 23 ++ embedfs/embedfs.go | 9 + embedfs/go.mod | 3 + frontend/.env.desktop | 1 + go.work | 6 + go.work.sum | 104 ++++++ openspec/specs/desktop-app/spec.md | 123 +++++++ scripts/build/package-macos.sh | 68 ++++ 22 files changed, 1297 insertions(+), 88 deletions(-) create mode 100644 Makefile create mode 100644 assets/AppIcon.icns create mode 100644 assets/README.md create mode 100644 assets/icon.ico create mode 100644 assets/icon.png create mode 100644 assets/icon.svg delete mode 100644 backend/Makefile create mode 100644 backend/cmd/desktop/main.go create mode 100644 backend/cmd/desktop/port_test.go create mode 100644 backend/cmd/desktop/singleton_test.go create mode 100644 backend/cmd/desktop/static_test.go create mode 100644 embedfs/embedfs.go create mode 100644 embedfs/go.mod create mode 100644 frontend/.env.desktop create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 openspec/specs/desktop-app/spec.md create mode 100755 scripts/build/package-macos.sh diff --git a/.gitignore b/.gitignore index b073885..bff243e 100644 --- a/.gitignore +++ b/.gitignore @@ -405,4 +405,9 @@ openspec/changes/archive temp .agents skills-lock.json -.worktrees \ No newline at end of file +.worktrees +!scripts/build/ + +# Embedfs generated +embedfs/assets/ +embedfs/frontend-dist/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0044793 --- /dev/null +++ b/Makefile @@ -0,0 +1,115 @@ +.PHONY: all clean \ + backend-build backend-run backend-test backend-test-unit backend-test-integration backend-test-coverage \ + backend-lint backend-deps backend-generate \ + backend-migrate-up backend-migrate-down backend-migrate-status backend-migrate-create \ + frontend-build frontend-dev frontend-test frontend-test-watch frontend-test-coverage frontend-test-e2e frontend-lint \ + desktop desktop-darwin desktop-windows desktop-linux package-macos + +# ============================================ +# 后端 +# ============================================ + +all: backend-build + +backend-build: + cd backend && go build -o bin/server ./cmd/server + +backend-run: + cd backend && go run ./cmd/server + +backend-test: + cd backend && go test ./... -v + +backend-test-unit: + cd backend && go test ./internal/... ./pkg/... -v + +backend-test-integration: + cd backend && go test ./tests/... -v + +backend-test-coverage: + cd backend && go test ./... -coverprofile=coverage.out + cd backend && go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: backend/coverage.html" + +backend-lint: + cd backend && go tool golangci-lint run ./... + +backend-deps: + cd backend && go mod tidy + +backend-generate: + cd backend && go generate ./... + +backend-migrate-up: + cd backend && goose -dir migrations sqlite3 $(DB_PATH) up + +backend-migrate-down: + cd backend && goose -dir migrations sqlite3 $(DB_PATH) down + +backend-migrate-status: + cd backend && goose -dir migrations sqlite3 $(DB_PATH) status + +backend-migrate-create: + @read -p "Migration name: " name; \ + cd backend && goose -dir migrations create $$name sql + +# ============================================ +# 前端 +# ============================================ + +frontend-build: + cd frontend && bun install && bun run build + +frontend-dev: + cd frontend && bun dev + +frontend-test: + cd frontend && bun run test + +frontend-test-watch: + cd frontend && bun run test:watch + +frontend-test-coverage: + cd frontend && bun run test:coverage + +frontend-test-e2e: + cd frontend && bun run test:e2e + +frontend-lint: + cd frontend && bun run lint + +# ============================================ +# 桌面应用 +# ============================================ + +desktop: frontend-build-desktop embedfs-prepare + cd backend && CGO_ENABLED=1 go build -o ../build/nex ./cmd/desktop + +frontend-build-desktop: + cd frontend && cp .env.desktop .env.production.local && bun install && bun run build && rm -f .env.production.local + +embedfs-prepare: + rm -rf embedfs/assets embedfs/frontend-dist + cp -r assets embedfs/assets + cp -r frontend/dist embedfs/frontend-dist + +desktop-darwin: frontend-build-desktop embedfs-prepare + cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../build/nex-darwin-arm64 ./cmd/desktop + cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../build/nex-darwin-amd64 ./cmd/desktop + +desktop-windows: frontend-build-desktop embedfs-prepare + cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o ../build/nex-windows-amd64.exe ./cmd/desktop + +desktop-linux: frontend-build-desktop embedfs-prepare + cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o ../build/nex-linux-amd64 ./cmd/desktop + +package-macos: + ./scripts/build/package-macos.sh + +# ============================================ +# 清理 +# ============================================ + +clean: + rm -rf backend/bin/ backend/coverage.out backend/coverage.html + rm -rf build/ diff --git a/README.md b/README.md index 946e6e2..bdedc85 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ ``` nex/ ├── backend/ # Go 后端服务(分层架构) -│ ├── cmd/server/ # 主程序入口 +│ ├── cmd/ +│ │ ├── server/ # CLI 主程序入口 +│ │ └── desktop/ # 桌面应用入口 │ ├── internal/ │ │ ├── handler/ # HTTP 处理器 + 中间件 │ │ ├── service/ # 业务逻辑层 @@ -32,6 +34,15 @@ nex/ │ ├── e2e/ # Playwright E2E 测试 │ └── package.json │ +├── assets/ # 应用资源 +│ ├── icon.png # 托盘图标 +│ ├── AppIcon.icns # macOS 应用图标 +│ └── icon.ico # Windows 应用图标 +│ +├── scripts/ # 构建脚本 +│ └── build/ +│ └── package-macos.sh # macOS .app 打包脚本 +│ └── README.md # 本文件 ``` @@ -72,7 +83,46 @@ nex/ ## 快速开始 -### 后端 +### 桌面应用(推荐) + +**构建桌面应用**: + +```bash +# 当前平台 +make desktop + +# macOS (arm64 + amd64) +make desktop-darwin +make package-macos # 打包为 .app + +# Windows +make desktop-windows + +# Linux +make desktop-linux +``` + +**使用桌面应用**: +- 双击启动应用(macOS: Nex.app,Windows: nex.exe,Linux: nex) +- 系统托盘图标出现,浏览器自动打开管理界面 +- 点击托盘图标显示菜单,可打开管理界面或退出 +- 关闭浏览器后服务继续运行,可通过托盘重新打开 + +**注意事项**: +- 桌面应用需要 CGO 支持 +- macOS: 自带 Xcode Command Line Tools +- Linux: 自带 gcc,部分桌面环境需要 `libappindicator3-dev` +- Windows: 需要 MinGW-w64 或在 Windows 环境构建 + +**Linux 桌面环境兼容性**: +- GNOME: 需要 AppIndicator 扩展 +- KDE Plasma: 原生支持 +- Xfce: 需要 libappindicator +- 其他支持 StatusNotifierItem 规范的环境 + +### CLI 模式 + +#### 后端 ```bash cd backend @@ -161,41 +211,24 @@ log: ## 测试 -### 后端测试 - ```bash -cd backend -make test # 运行所有测试 -make test-coverage # 生成覆盖率报告 -``` - -### 前端测试 - -```bash -cd frontend -bun run test # 单元测试 + 组件测试 -bun run test:watch # 监听模式 -bun run test:coverage # 生成覆盖率报告 -bun run test:e2e # E2E 测试 +make backend-test # 后端测试 +make backend-test-coverage # 后端覆盖率 +make frontend-test # 前端测试 +make frontend-test-e2e # 前端 E2E 测试 ``` ## 开发 -### 后端开发 - ```bash -cd backend -make build # 构建 -make lint # 代码检查 -make migrate-up # 数据库迁移 -``` +make backend-build # 构建后端 +make backend-run # 运行后端 +make backend-lint # 后端代码检查 +make backend-migrate-up # 数据库迁移 -### 前端开发 - -```bash -cd frontend -bun run build # 构建生产版本 -bun run lint # 代码检查 +make frontend-build # 构建前端 +make frontend-dev # 前端开发模式 +make frontend-lint # 前端代码检查 ``` ## 开发规范 diff --git a/assets/AppIcon.icns b/assets/AppIcon.icns new file mode 100644 index 0000000000000000000000000000000000000000..47645c0a662aadb728786bec1263356b7b32d5bc GIT binary patch literal 91424 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

Lblq1vfQ(fML!gmg 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 literal 0 HcmV?d00001 diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..102c2c8 --- /dev/null +++ b/assets/README.md @@ -0,0 +1,64 @@ +# Assets + +应用资源文件目录。 + +## 文件说明 + +| 文件 | 用途 | 尺寸 | 格式 | +|------|------|------|------| +| `icon.svg` | 源图标 | 64x64 | SVG | +| `icon.png` | 托盘图标 | 64x64 | PNG | +| `AppIcon.icns` | macOS 应用图标 | 多尺寸 | ICNS | +| `icon.ico` | Windows 应用图标 | 256x256 | ICO | + +## 替换图标 + +### 1. 准备图标 + +推荐使用 SVG 格式的源图标,尺寸至少 256x256。 + +### 2. 生成各平台图标 + +**托盘图标 (PNG)**: +```bash +magick your-icon.svg -resize 64x64 icon.png +``` + +**macOS 应用图标 (ICNS)**: +```bash +mkdir icon.iconset +magick your-icon.svg -resize 16x16 icon.iconset/icon_16x16.png +magick your-icon.svg -resize 32x32 icon.iconset/icon_16x16@2x.png +magick your-icon.svg -resize 32x32 icon.iconset/icon_32x32.png +magick your-icon.svg -resize 64x64 icon.iconset/icon_32x32@2x.png +magick your-icon.svg -resize 128x128 icon.iconset/icon_128x128.png +magick your-icon.svg -resize 256x256 icon.iconset/icon_128x128@2x.png +iconutil -c icns icon.iconset -o AppIcon.icns +rm -rf icon.iconset +``` + +**Windows 应用图标 (ICO)**: +```bash +magick your-icon.svg -resize 256x256 icon.ico +``` + +### 3. 替换文件 + +将生成的文件放入此目录,然后重新构建桌面应用: +```bash +./scripts/build/build-darwin-arm64.sh +``` + +## macOS Template 图标 + +macOS 支持 Template 图标,自动适配深浅色模式: +- 使用黑色 + 透明设计 +- 文件名以 `Template` 结尾(如 `iconTemplate.png`) +- 黑色在深色模式下自动变为白色 + +## 设计建议 + +- 托盘图标应简洁,在小尺寸下清晰可辨 +- 避免过多细节和文字 +- 使用高对比度颜色 +- macOS 建议使用 Template 图标风格 diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7bc6e0f8927f4cb4a8a26745baa7a211b01b2105 GIT binary patch literal 270398 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 + + + + + + + + + + + + diff --git a/backend/Makefile b/backend/Makefile deleted file mode 100644 index 0003a85..0000000 --- a/backend/Makefile +++ /dev/null @@ -1,57 +0,0 @@ -.PHONY: build run test test-unit test-integration test-coverage clean migrate-up migrate-down migrate-status migrate-create lint generate deps - -# 构建 -build: - go build -o bin/server ./cmd/server - -# 运行 -run: - go run ./cmd/server - -# 测试 -test: - go test ./... -v - -# 单元测试 -test-unit: - go test ./internal/... ./pkg/... -v - -# 集成测试 -test-integration: - go test ./tests/... -v - -# 测试覆盖率 -test-coverage: - go test ./... -coverprofile=coverage.out - go tool cover -html=coverage.out -o coverage.html - @echo "Coverage report generated: coverage.html" - -# 清理 -clean: - rm -rf bin/ coverage.out coverage.html - -# 数据库迁移 -migrate-up: - goose -dir migrations sqlite3 $(DB_PATH) up - -migrate-down: - goose -dir migrations sqlite3 $(DB_PATH) down - -migrate-status: - goose -dir migrations sqlite3 $(DB_PATH) status - -migrate-create: - @read -p "Migration name: " name; \ - goose -dir migrations create $$name sql - -# 代码检查 -lint: - go tool golangci-lint run ./... - -# 安装依赖 -deps: - go mod tidy - -# 生成代码(mock 等) -generate: - go generate ./... diff --git a/backend/cmd/desktop/main.go b/backend/cmd/desktop/main.go new file mode 100644 index 0000000..6721bb8 --- /dev/null +++ b/backend/cmd/desktop/main.go @@ -0,0 +1,456 @@ +package main + +import ( + "context" + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/getlantern/systray" + "github.com/pressly/goose/v3" + "go.uber.org/zap" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "nex/backend/internal/config" + "nex/backend/internal/conversion" + "nex/backend/internal/conversion/anthropic" + "nex/backend/internal/conversion/openai" + "nex/backend/internal/handler" + "nex/backend/internal/handler/middleware" + "nex/backend/internal/provider" + "nex/backend/internal/repository" + "nex/backend/internal/service" + pkgLogger "nex/backend/pkg/logger" + + "nex/embedfs" +) + +var ( + server *http.Server + zapLogger *zap.Logger + shutdownCtx context.Context + shutdownCancel context.CancelFunc +) + +func main() { + port := 9826 + + if err := acquireSingleInstance(); err != nil { + showError("Nex Gateway", "已有 Nex 实例运行") + os.Exit(1) + } + defer releaseSingleInstance() + + if err := checkPortAvailable(port); err != nil { + showError("Nex Gateway", err.Error()) + os.Exit(1) + } + + cfg, err := config.LoadConfig() + if err != nil { + showError("Nex Gateway", fmt.Sprintf("加载配置失败: %v", err)) + os.Exit(1) + } + + zapLogger, err = pkgLogger.New(pkgLogger.Config{ + Level: cfg.Log.Level, + Path: cfg.Log.Path, + MaxSize: cfg.Log.MaxSize, + MaxBackups: cfg.Log.MaxBackups, + MaxAge: cfg.Log.MaxAge, + Compress: cfg.Log.Compress, + }) + if err != nil { + showError("Nex Gateway", fmt.Sprintf("初始化日志失败: %v", err)) + os.Exit(1) + } + defer zapLogger.Sync() + + db, err := initDatabase(cfg) + if err != nil { + showError("Nex Gateway", fmt.Sprintf("初始化数据库失败: %v", err)) + os.Exit(1) + } + defer closeDB(db) + + providerRepo := repository.NewProviderRepository(db) + modelRepo := repository.NewModelRepository(db) + statsRepo := repository.NewStatsRepository(db) + + providerService := service.NewProviderService(providerRepo, modelRepo) + modelService := service.NewModelService(modelRepo, providerRepo) + routingService := service.NewRoutingService(modelRepo, providerRepo) + statsService := service.NewStatsService(statsRepo) + + registry := conversion.NewMemoryRegistry() + if err := registry.Register(openai.NewAdapter()); err != nil { + zapLogger.Fatal("注册 OpenAI 适配器失败", zap.String("error", err.Error())) + } + if err := registry.Register(anthropic.NewAdapter()); err != nil { + zapLogger.Fatal("注册 Anthropic 适配器失败", zap.String("error", err.Error())) + } + engine := conversion.NewConversionEngine(registry, zapLogger) + + providerClient := provider.NewClient() + + proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService) + providerHandler := handler.NewProviderHandler(providerService) + modelHandler := handler.NewModelHandler(modelService) + statsHandler := handler.NewStatsHandler(statsService) + + gin.SetMode(gin.ReleaseMode) + r := gin.New() + + r.Use(middleware.RequestID()) + r.Use(middleware.Recovery(zapLogger)) + r.Use(middleware.Logging(zapLogger)) + r.Use(middleware.CORS()) + + setupRoutes(r, proxyHandler, providerHandler, modelHandler, statsHandler) + setupStaticFiles(r) + + server = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: r, + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + } + + shutdownCtx, shutdownCancel = context.WithCancel(context.Background()) + + go func() { + zapLogger.Info("AI Gateway 启动", zap.String("addr", server.Addr)) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + zapLogger.Fatal("服务器启动失败", zap.String("error", err.Error())) + } + }() + + go func() { + time.Sleep(500 * time.Millisecond) + if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil { + zapLogger.Warn("无法打开浏览器", zap.String("error", err.Error())) + } + }() + + setupSystray(port) +} + +func initDatabase(cfg *config.Config) (*gorm.DB, error) { + dbDir := filepath.Dir(cfg.Database.Path) + if err := os.MkdirAll(dbDir, 0755); err != nil { + return nil, fmt.Errorf("创建数据库目录失败: %w", err) + } + + db, err := gorm.Open(sqlite.Open(cfg.Database.Path), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + return nil, err + } + + if err := runMigrations(db); err != nil { + return nil, fmt.Errorf("数据库迁移失败: %w", err) + } + + if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil { + log.Printf("警告: 启用 WAL 模式失败: %v", err) + } + + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns) + sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns) + sqlDB.SetConnMaxLifetime(cfg.Database.ConnMaxLifetime) + + return db, nil +} + +func runMigrations(db *gorm.DB) error { + sqlDB, err := db.DB() + if err != nil { + return err + } + + migrationsDir := getMigrationsDir() + if _, err := os.Stat(migrationsDir); os.IsNotExist(err) { + return fmt.Errorf("迁移目录不存在: %s", migrationsDir) + } + + goose.SetDialect("sqlite3") + if err := goose.Up(sqlDB, migrationsDir); err != nil { + return err + } + + return nil +} + +func getMigrationsDir() string { + _, filename, _, ok := runtime.Caller(0) + if ok { + dir := filepath.Join(filepath.Dir(filename), "..", "..", "migrations") + if abs, err := filepath.Abs(dir); err == nil { + return abs + } + } + return "./migrations" +} + +func closeDB(db *gorm.DB) { + sqlDB, err := db.DB() + if err != nil { + return + } + sqlDB.Close() +} + +func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHandler *handler.ProviderHandler, modelHandler *handler.ModelHandler, statsHandler *handler.StatsHandler) { + r.Any("/v1/*path", proxyHandler.HandleProxy) + + providers := r.Group("/api/providers") + { + providers.GET("", providerHandler.ListProviders) + providers.POST("", providerHandler.CreateProvider) + providers.GET("/:id", providerHandler.GetProvider) + providers.PUT("/:id", providerHandler.UpdateProvider) + providers.DELETE("/:id", providerHandler.DeleteProvider) + } + + models := r.Group("/api/models") + { + models.GET("", modelHandler.ListModels) + models.POST("", modelHandler.CreateModel) + models.GET("/:id", modelHandler.GetModel) + models.PUT("/:id", modelHandler.UpdateModel) + models.DELETE("/:id", modelHandler.DeleteModel) + } + + stats := r.Group("/api/stats") + { + stats.GET("", statsHandler.GetStats) + stats.GET("/aggregate", statsHandler.AggregateStats) + } + + r.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{"status": "ok"}) + }) +} + +func setupStaticFiles(r *gin.Engine) { + distFS, err := fs.Sub(embedfs.FrontendDist, "frontend-dist") + if err != nil { + zapLogger.Fatal("无法加载前端资源", zap.String("error", err.Error())) + } + + getContentType := func(path string) string { + if strings.HasSuffix(path, ".js") { + return "application/javascript" + } + if strings.HasSuffix(path, ".css") { + return "text/css" + } + if strings.HasSuffix(path, ".svg") { + return "image/svg+xml" + } + if strings.HasSuffix(path, ".png") { + return "image/png" + } + if strings.HasSuffix(path, ".ico") { + return "image/x-icon" + } + if strings.HasSuffix(path, ".woff") || strings.HasSuffix(path, ".woff2") { + return "font/woff2" + } + return "application/octet-stream" + } + + r.GET("/assets/*filepath", func(c *gin.Context) { + filepath := c.Param("filepath") + data, err := fs.ReadFile(distFS, "assets"+filepath) + if err != nil { + c.Status(404) + return + } + c.Data(200, getContentType(filepath), data) + }) + + r.GET("/favicon.svg", func(c *gin.Context) { + data, err := fs.ReadFile(distFS, "favicon.svg") + if err != nil { + c.Status(404) + return + } + c.Data(200, "image/svg+xml", data) + }) + + r.NoRoute(func(c *gin.Context) { + path := c.Request.URL.Path + + if strings.HasPrefix(path, "/api/") || + strings.HasPrefix(path, "/v1/") || + strings.HasPrefix(path, "/health") { + c.JSON(404, gin.H{"error": "not found"}) + return + } + + data, err := fs.ReadFile(distFS, "index.html") + if err != nil { + c.Status(500) + return + } + c.Data(200, "text/html; charset=utf-8", data) + }) +} + +func setupSystray(port int) { + systray.Run(func() { + icon, err := embedfs.Assets.ReadFile("assets/icon.png") + if err != nil { + zapLogger.Error("无法加载托盘图标", zap.String("error", err.Error())) + } + systray.SetIcon(icon) + systray.SetTitle("Nex Gateway") + systray.SetTooltip("AI Gateway") + + mOpen := systray.AddMenuItem("打开管理界面", "在浏览器中打开") + systray.AddSeparator() + mStatus := systray.AddMenuItem("状态: 运行中", "") + mStatus.Disable() + mPort := systray.AddMenuItem(fmt.Sprintf("端口: %d", port), "") + mPort.Disable() + systray.AddSeparator() + mAbout := systray.AddMenuItem("关于", "") + systray.AddSeparator() + mQuit := systray.AddMenuItem("退出", "停止服务并退出") + + go func() { + for { + select { + case <-mOpen.ClickedCh: + openBrowser(fmt.Sprintf("http://localhost:%d", port)) + case <-mAbout.ClickedCh: + showAbout() + case <-mQuit.ClickedCh: + doShutdown() + systray.Quit() + return + } + } + }() + }, nil) +} + +func doShutdown() { + if zapLogger != nil { + zapLogger.Info("正在关闭服务器...") + } + + if server != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + server.Shutdown(ctx) + } + + if shutdownCancel != nil { + shutdownCancel() + } +} + +func checkPortAvailable(port int) error { + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return fmt.Errorf("端口 %d 已被占用\n\n可能原因:\n- 已有 Nex 实例运行\n- 其他程序占用了该端口\n\n请检查并关闭占用端口的程序", port) + } + ln.Close() + return nil +} + +var lockFile *os.File + +func acquireSingleInstance() error { + lockPath := filepath.Join(os.TempDir(), "nex-gateway.lock") + + f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return err + } + + err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if err != nil { + f.Close() + return fmt.Errorf("已有实例运行") + } + + lockFile = f + return nil +} + +func releaseSingleInstance() { + if lockFile != nil { + syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN) + lockFile.Close() + } +} + +func openBrowser(url string) error { + var cmd *exec.Cmd + + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + case "linux": + browsers := []string{"xdg-open", "google-chrome", "firefox"} + for _, browser := range browsers { + if _, err := exec.LookPath(browser); err == nil { + cmd = exec.Command(browser, url) + break + } + } + } + + if cmd == nil { + return fmt.Errorf("无法打开浏览器") + } + + return cmd.Start() +} + +func showError(title, message string) { + switch runtime.GOOS { + case "darwin": + script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`, message, title) + exec.Command("osascript", "-e", script).Run() + case "windows": + exec.Command("msg", "*", message).Run() + case "linux": + exec.Command("zenity", "--error", fmt.Sprintf("--title=%s", title), fmt.Sprintf("--text=%s", message)).Run() + } +} + +func showAbout() { + message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway" + switch runtime.GOOS { + case "darwin": + script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "关于 Nex Gateway"`, message) + exec.Command("osascript", "-e", script).Run() + case "windows": + exec.Command("msg", "*", message).Run() + case "linux": + exec.Command("zenity", "--info", "--title=关于 Nex Gateway", fmt.Sprintf("--text=%s", message)).Run() + } +} diff --git a/backend/cmd/desktop/port_test.go b/backend/cmd/desktop/port_test.go new file mode 100644 index 0000000..cd810d3 --- /dev/null +++ b/backend/cmd/desktop/port_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "net" + "net/http" + "testing" + "time" +) + +func TestCheckPortAvailable(t *testing.T) { + port := 19826 + + err := checkPortAvailable(port) + if err != nil { + t.Fatalf("端口 %d 应该可用: %v", port, err) + } + + t.Log("端口可用测试通过") +} + +func TestCheckPortOccupied(t *testing.T) { + port := 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) + if err == nil { + t.Fatal("端口被占用时应该返回错误") + } + + t.Log("端口占用检测测试通过") +} + +func TestCheckPortAvailableAfterClose(t *testing.T) { + port := 19828 + + listener, err := net.Listen("tcp", ":19828") + if err != nil { + t.Fatalf("无法启动测试服务器: %v", err) + } + + server := &http.Server{} + go server.Serve(listener) + + time.Sleep(100 * time.Millisecond) + + listener.Close() + time.Sleep(100 * time.Millisecond) + + err = checkPortAvailable(port) + if err != nil { + t.Fatalf("端口关闭后应该可用: %v", err) + } + + t.Log("端口关闭后可用测试通过") +} diff --git a/backend/cmd/desktop/singleton_test.go b/backend/cmd/desktop/singleton_test.go new file mode 100644 index 0000000..03031cf --- /dev/null +++ b/backend/cmd/desktop/singleton_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + "path/filepath" + "syscall" + "testing" +) + +func TestAcquireSingleInstance(t *testing.T) { + lockPath := filepath.Join(os.TempDir(), "nex-gateway-test.lock") + + origLockFile := lockFile + lockFile = nil + defer func() { lockFile = origLockFile }() + + f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + t.Fatalf("无法创建锁文件: %v", err) + } + defer f.Close() + defer os.Remove(lockPath) + + err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if err != nil { + t.Fatalf("无法获取文件锁: %v", err) + } + defer syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + + t.Log("单实例锁测试通过") +} + +func TestReleaseSingleInstance(t *testing.T) { + lockFile = nil + + releaseSingleInstance() + + t.Log("释放空锁测试通过") +} diff --git a/backend/cmd/desktop/static_test.go b/backend/cmd/desktop/static_test.go new file mode 100644 index 0000000..f3bac4b --- /dev/null +++ b/backend/cmd/desktop/static_test.go @@ -0,0 +1,123 @@ +package main + +import ( + "io/fs" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + + "nex/embedfs" +) + +func TestSetupStaticFiles(t *testing.T) { + gin.SetMode(gin.TestMode) + + distFS, err := fs.Sub(embedfs.FrontendDist, "frontend-dist") + if err != nil { + t.Skipf("跳过测试: 前端资源未构建: %v", err) + return + } + + getContentType := func(path string) string { + if strings.HasSuffix(path, ".js") { + return "application/javascript" + } + if strings.HasSuffix(path, ".css") { + return "text/css" + } + if strings.HasSuffix(path, ".svg") { + return "image/svg+xml" + } + return "application/octet-stream" + } + + r := gin.New() + r.GET("/assets/*filepath", func(c *gin.Context) { + filepath := c.Param("filepath") + data, err := fs.ReadFile(distFS, "assets"+filepath) + if err != nil { + c.Status(404) + return + } + c.Data(200, getContentType(filepath), data) + }) + + r.GET("/favicon.svg", func(c *gin.Context) { + data, err := fs.ReadFile(distFS, "favicon.svg") + if err != nil { + c.Status(404) + return + } + c.Data(200, "image/svg+xml", data) + }) + + r.NoRoute(func(c *gin.Context) { + path := c.Request.URL.Path + if strings.HasPrefix(path, "/api/") || + strings.HasPrefix(path, "/v1/") || + strings.HasPrefix(path, "/health") { + c.JSON(404, gin.H{"error": "not found"}) + return + } + data, err := fs.ReadFile(distFS, "index.html") + if err != nil { + c.Status(500) + return + } + c.Data(200, "text/html; charset=utf-8", data) + }) + + t.Run("API 404", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/test", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != 404 { + t.Errorf("期望状态码 404, 实际 %d", w.Code) + } + }) + + t.Run("SPA fallback", func(t *testing.T) { + req := httptest.NewRequest("GET", "/providers", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("期望状态码 200, 实际 %d", w.Code) + } + }) + + t.Run("MIME type for JS", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/test.js", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code == 200 { + expected := "application/javascript" + if w.Header().Get("Content-Type") != expected { + t.Errorf("期望 Content-Type %s, 实际 %s", expected, w.Header().Get("Content-Type")) + } + } else { + t.Log("文件不存在,跳过 MIME 类型验证") + } + }) + + t.Run("MIME type for CSS", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/test.css", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code == 200 { + expected := "text/css" + if w.Header().Get("Content-Type") != expected { + t.Errorf("期望 Content-Type %s, 实际 %s", expected, w.Header().Get("Content-Type")) + } + } else { + t.Log("文件不存在,跳过 MIME 类型验证") + } + }) + + t.Log("静态文件服务测试通过") +} diff --git a/backend/go.mod b/backend/go.mod index 8a59480..99e3d99 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,12 +2,15 @@ module nex/backend go 1.26.2 +replace nex/embedfs => ../embedfs + tool ( github.com/golangci/golangci-lint/cmd/golangci-lint go.uber.org/mock/mockgen ) require ( + github.com/getlantern/systray v1.2.2 github.com/gin-gonic/gin v1.12.0 github.com/go-playground/validator/v10 v10.30.2 github.com/google/uuid v1.6.0 @@ -22,6 +25,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 + nex/embedfs v0.0.0-00010101000000-000000000000 ) require ( @@ -74,11 +78,18 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect + github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect + github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect + github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect + github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect + github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect github.com/ghostiam/protogetter v0.3.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-critic/go-critic v0.12.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-stack/stack v1.8.0 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -153,6 +164,7 @@ require ( github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.19.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.7.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 4018f0a..d6f1911 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -163,6 +163,20 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= +github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk= +github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= +github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE= +github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE= github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ= github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -192,6 +206,7 @@ github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -397,6 +412,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= @@ -460,6 +477,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -551,6 +570,7 @@ github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+W github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -819,6 +839,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -832,6 +853,7 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1008,6 +1030,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/embedfs/embedfs.go b/embedfs/embedfs.go new file mode 100644 index 0000000..988f5ee --- /dev/null +++ b/embedfs/embedfs.go @@ -0,0 +1,9 @@ +package embedfs + +import "embed" + +//go:embed assets/* +var Assets embed.FS + +//go:embed frontend-dist/* +var FrontendDist embed.FS diff --git a/embedfs/go.mod b/embedfs/go.mod new file mode 100644 index 0000000..fa30a0a --- /dev/null +++ b/embedfs/go.mod @@ -0,0 +1,3 @@ +module nex/embedfs + +go 1.26.2 diff --git a/frontend/.env.desktop b/frontend/.env.desktop new file mode 100644 index 0000000..a41b3e9 --- /dev/null +++ b/frontend/.env.desktop @@ -0,0 +1 @@ +VITE_API_BASE= diff --git a/go.work b/go.work new file mode 100644 index 0000000..d78f82a --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.26.2 + +use ( + backend + embedfs +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..defc4aa --- /dev/null +++ b/go.work.sum @@ -0,0 +1,104 @@ +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= +github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/cristalhq/acmd v0.12.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= +github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golangci/modinfo v0.3.3/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM= +github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE= +github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= +github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ= +github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM= +github.com/vertica/vertica-sql-go v1.3.5/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= +github.com/ydb-platform/ydb-go-sdk/v3 v3.127.0/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/api v0.223.0/go.mod h1:C+RS7Z+dDwds2b+zoAk5hN/eSfsiCn0UDrYof/M4d2M= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/openspec/specs/desktop-app/spec.md b/openspec/specs/desktop-app/spec.md new file mode 100644 index 0000000..9758f23 --- /dev/null +++ b/openspec/specs/desktop-app/spec.md @@ -0,0 +1,123 @@ +# 桌面应用 + +## Purpose + +TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源打包为单一可执行文件 + +## Requirements + +### Requirement: 桌面应用启动 + +系统 SHALL 支持作为桌面应用启动,将后端服务与前端静态资源打包为单一可执行文件。 + +#### Scenario: 双击启动 +- **WHEN** 用户双击桌面应用可执行文件 +- **THEN** 系统启动后端服务 +- **AND** 系统托盘图标出现 +- **AND** 浏览器自动打开 `http://localhost:9826` 显示管理界面 + +#### Scenario: 单实例检查 +- **WHEN** 用户尝试启动第二个实例 +- **THEN** 系统检测到已有实例运行 +- **AND** 显示错误提示"已有 Nex 实例运行" +- **AND** 新实例退出 + +### Requirement: 系统托盘 + +系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。 + +#### Scenario: 托盘图标显示 +- **WHEN** 桌面应用启动成功 +- **THEN** 系统托盘区域显示应用图标 +- **AND** 托盘图标 tooltip 显示"AI Gateway" + +#### Scenario: 托盘菜单显示 +- **WHEN** 用户点击托盘图标(左键或右键) +- **THEN** 显示托盘菜单 +- **AND** 菜单包含"打开管理界面"选项 +- **AND** 菜单包含"状态: 运行中"选项(禁用状态) +- **AND** 菜单包含"端口: 9826"选项(禁用状态) +- **AND** 菜单包含"关于"选项 +- **AND** 菜单包含"退出"选项 + +#### Scenario: 打开管理界面 +- **WHEN** 用户点击托盘菜单"打开管理界面" +- **THEN** 系统在浏览器中打开 `http://localhost:9826` + +#### Scenario: 浏览器打开失败 +- **WHEN** 系统无法打开浏览器(浏览器未安装等) +- **THEN** 托盘菜单仍可正常使用 +- **AND** 用户可手动访问 `http://localhost:9826` + +#### Scenario: 退出应用 +- **WHEN** 用户点击托盘菜单"退出" +- **THEN** 系统优雅关闭后端服务 +- **AND** 托盘图标消失 +- **AND** 应用进程退出 + +### Requirement: 静态文件服务 + +系统 SHALL 通过 Gin 同时服务 API 和前端静态资源。 + +#### Scenario: API 请求路由 +- **WHEN** 请求路径以 `/api/` 或 `/v1/` 开头 +- **THEN** 请求由现有业务 handler 处理 + +#### Scenario: 静态资源路由 +- **WHEN** 请求路径为 `/assets/*` +- **THEN** 返回嵌入的前端静态资源文件 + +#### Scenario: SPA 路由回退 +- **WHEN** 请求路径不匹配任何 API 或静态资源路由 +- **THEN** 返回 `index.html`(支持前端 SPA 路由) + +### Requirement: 端口冲突检测 + +系统 SHALL 在启动前检测端口是否可用。 + +#### Scenario: 端口可用 +- **WHEN** 端口 9826 未被占用 +- **THEN** 服务正常启动 + +#### Scenario: 端口被占用 +- **WHEN** 端口 9826 已被其他程序占用 +- **THEN** 显示错误提示"端口 9826 已被占用" +- **AND** 应用退出 + +### Requirement: 跨平台构建 + +系统 SHALL 支持跨平台构建和打包。 + +#### Scenario: macOS 构建 +- **WHEN** 执行 macOS 构建命令 +- **THEN** 生成 `nex-darwin-arm64` 和 `nex-darwin-amd64` 可执行文件 +- **AND** 可打包为 `.app` bundle + +#### Scenario: Windows 构建 +- **WHEN** 执行 Windows 构建命令 +- **THEN** 生成 `nex-windows-amd64.exe` 可执行文件 +- **AND** 运行时不显示控制台窗口 + +#### Scenario: Linux 构建 +- **WHEN** 执行 Linux 构建命令 +- **THEN** 生成 `nex-linux-amd64` 可执行文件 + +### Requirement: macOS .app 打包 + +系统 SHALL 支持打包为 macOS .app bundle。 + +#### Scenario: .app 结构 +- **WHEN** 执行打包脚本 +- **THEN** 生成 `Nex.app` 目录结构 +- **AND** 包含 `Contents/Info.plist` 元数据 +- **AND** 包含 `Contents/MacOS/nex` 可执行文件 +- **AND** 包含 `Contents/Resources/AppIcon.icns` 图标 +- **AND** `Info.plist` 中 `LSUIElement` 为 `true`(不显示 Dock 图标) + +### Requirement: 关于对话框 + +系统 SHALL 提供关于对话框显示应用信息。 + +#### Scenario: 显示关于 +- **WHEN** 用户点击托盘菜单"关于" +- **THEN** 显示对话框包含应用名称、项目链接 diff --git a/scripts/build/package-macos.sh b/scripts/build/package-macos.sh new file mode 100755 index 0000000..41fed5d --- /dev/null +++ b/scripts/build/package-macos.sh @@ -0,0 +1,68 @@ +#!/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-darwin-arm64" ]; then + cp "${BUILD_DIR}/nex-darwin-arm64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex" +elif [ -f "${BUILD_DIR}/nex-darwin-amd64" ]; then + cp "${BUILD_DIR}/nex-darwin-amd64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex" +else + echo "错误: 未找到 macOS 二进制文件,请先运行 make desktop-darwin" + 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 + + + + + CFBundleDevelopmentRegion + zh_CN + CFBundleExecutable + nex + CFBundleIconFile + AppIcon + CFBundleIdentifier + ${BUNDLE_ID} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${APP_NAME} Gateway + CFBundleDisplayName + ${APP_NAME} Gateway + CFBundlePackageType + APPL + CFBundleShortVersionString + ${VERSION} + CFBundleVersion + ${VERSION} + LSMinimumSystemVersion + 10.13 + LSUIElement + + NSHighResolutionCapable + + + +EOF + +chmod +x "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex" + +echo "打包完成: ${BUILD_DIR}/${APP_NAME}.app"