From cbdcbed7a06c68785e59f96ba9f33e4d8c4546b8 Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Mon, 18 May 2026 17:51:40 +0800 Subject: [PATCH] feat(deps): implement batch installation and manifest-based (requirements.txt/package.json) parser and importer --- internal/controllers/dependency_controller.go | 94 ++++++++++++++++ internal/router/api_routes.go | 2 + internal/services/dependency_service.go | 27 +++++ internal/services/deps/manager.go | 28 +++++ internal/services/deps/parser.go | 105 ++++++++++++++++++ internal/services/deps/parser_test.go | 87 +++++++++++++++ 6 files changed, 343 insertions(+) create mode 100644 internal/services/deps/parser.go create mode 100644 internal/services/deps/parser_test.go diff --git a/internal/controllers/dependency_controller.go b/internal/controllers/dependency_controller.go index 991acc5..1b4d085 100644 --- a/internal/controllers/dependency_controller.go +++ b/internal/controllers/dependency_controller.go @@ -6,6 +6,7 @@ import ( "github.com/engigu/baihu-panel/internal/models" "github.com/engigu/baihu-panel/internal/models/vo" "github.com/engigu/baihu-panel/internal/services" + "github.com/engigu/baihu-panel/internal/services/deps" "github.com/engigu/baihu-panel/internal/utils" "github.com/gin-gonic/gin" @@ -303,3 +304,96 @@ func (c *DependencyController) GetInstalled(ctx *gin.Context) { utils.Success(ctx, packages) } + +// GetBatchInstallCommand 获取批量安装依赖包的命令 +func (c *DependencyController) GetBatchInstallCommand(ctx *gin.Context) { + var req struct { + Items []struct { + Name string `json:"name" binding:"required"` + Version string `json:"version"` + Language string `json:"language" binding:"required"` + LangVersion string `json:"lang_version"` + } `json:"items" binding:"required,gt=0"` + } + + if err := ctx.ShouldBindJSON(&req); err != nil { + utils.BadRequest(ctx, "参数错误: items 不能为空且必须包含 name 和 language") + return + } + + var depsList []models.Dependency + for _, item := range req.Items { + depsList = append(depsList, models.Dependency{ + Name: item.Name, + Version: item.Version, + Language: item.Language, + LangVersion: item.LangVersion, + }) + } + + cmd, err := c.service.GetBatchInstallCommand(depsList) + if err != nil { + utils.ServerError(ctx, err.Error()) + return + } + + utils.Success(ctx, gin.H{"command": cmd}) +} + +// ParseAndImport 解析上传/粘贴的清单文件内容并批量导入至数据库 +func (c *DependencyController) ParseAndImport(ctx *gin.Context) { + var req struct { + Language string `json:"language" binding:"required"` + LangVersion string `json:"lang_version"` + Content string `json:"content" binding:"required"` + ImportDB bool `json:"import_db"` // 是否持久化到数据库做可视化管理 + } + + if err := ctx.ShouldBindJSON(&req); err != nil { + utils.BadRequest(ctx, "参数错误: language 和 content 必填") + return + } + + // 1. 解析文本清单内容 + parsedDeps, err := deps.ParseManifest(req.Language, req.Content) + if err != nil { + utils.ServerError(ctx, "清单文件解析失败: "+err.Error()) + return + } + + if len(parsedDeps) == 0 { + utils.BadRequest(ctx, "未解析到任何有效依赖包") + return + } + + // 2. 补全语言和版本属性 + for i := range parsedDeps { + parsedDeps[i].Language = req.Language + parsedDeps[i].LangVersion = req.LangVersion + } + + // 3. 根据需求决定是否导入数据库 + var finalDeps []models.Dependency + if req.ImportDB { + imported, err := c.service.ImportDependencies(parsedDeps) + if err != nil { + utils.ServerError(ctx, "导入依赖记录至数据库失败: "+err.Error()) + return + } + finalDeps = imported + } else { + finalDeps = parsedDeps + } + + // 4. 为这一批包生成合并批量安装命令 + cmd, err := c.service.GetBatchInstallCommand(finalDeps) + if err != nil { + utils.ServerError(ctx, "生成安装命令失败: "+err.Error()) + return + } + + utils.Success(ctx, gin.H{ + "dependencies": vo.ToDependencyVOListFromModels(finalDeps), + "command": cmd, + }) +} diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go index e0dd5cc..6b716db 100644 --- a/internal/router/api_routes.go +++ b/internal/router/api_routes.go @@ -193,6 +193,8 @@ func registerDependencyRoutes(g *gin.RouterGroup, c *Controllers) { deps.POST("/reinstall/:id", c.Dependency.Reinstall) deps.POST("/reinstall-all", c.Dependency.ReinstallAll) deps.POST("/reinstall-all-cmd", c.Dependency.GetReinstallAllCommand) + deps.POST("/batch-install-cmd", c.Dependency.GetBatchInstallCommand) + deps.POST("/import", c.Dependency.ParseAndImport) deps.GET("/installed", c.Dependency.GetInstalled) } } diff --git a/internal/services/dependency_service.go b/internal/services/dependency_service.go index dc9ef77..58b0fb6 100644 --- a/internal/services/dependency_service.go +++ b/internal/services/dependency_service.go @@ -111,3 +111,30 @@ func (s *DependencyService) GetVerifyCommand(language, langVersion string) (stri } return m.GetVerifyCommand(langVersion) } + +// GetBatchInstallCommand 获取批量安装命令 +func (s *DependencyService) GetBatchInstallCommand(depsList []models.Dependency) (string, error) { + if len(depsList) == 0 { + return "", errors.New("依赖包列表不能为空") + } + + firstDep := depsList[0] + m := deps.GetManager(firstDep.Language) + if m == nil { + return "", errors.New("不支持的依赖类型: " + firstDep.Language) + } + + return m.GetBatchInstallCommand(depsList) +} + +// ImportDependencies 批量导入依赖并自动入库去重 +func (s *DependencyService) ImportDependencies(depsList []models.Dependency) ([]models.Dependency, error) { + var imported []models.Dependency + for i := range depsList { + dep := &depsList[i] + if err := s.Create(dep); err == nil { + imported = append(imported, *dep) + } + } + return imported, nil +} diff --git a/internal/services/deps/manager.go b/internal/services/deps/manager.go index f8db8e2..0f44b8c 100644 --- a/internal/services/deps/manager.go +++ b/internal/services/deps/manager.go @@ -16,6 +16,7 @@ type Manager interface { Uninstall(dep *models.Dependency) error GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) GetInstallCommand(dep *models.Dependency) (string, error) + GetBatchInstallCommand(deps []models.Dependency) (string, error) GetReinstallAllCommand(deps []models.Dependency) (string, error) GetVerifyCommand(langVersion string) (string, error) } @@ -74,6 +75,33 @@ func (m *BaseManager) GetInstallCommand(dep *models.Dependency) (string, error) return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil } +func (m *BaseManager) GetBatchInstallCommand(deps []models.Dependency) (string, error) { + if len(deps) == 0 { + return "echo \"没有需要安装的依赖\"", nil + } + + var packageSpecs []string + var langVersion string + var language string + for _, dep := range deps { + language = dep.Language + if dep.LangVersion != "" { + langVersion = dep.LangVersion + } + if dep.Version != "" { + packageSpecs = append(packageSpecs, dep.Name+m.Separator+dep.Version) + } else { + packageSpecs = append(packageSpecs, dep.Name) + } + } + + args := append([]string{}, m.InstallCmd...) + args = append(args, packageSpecs...) + + fullCmd := utils.BuildMiseCommandSimple(strings.Join(args, " "), language, langVersion) + return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil +} + func (m *BaseManager) GetReinstallAllCommand(deps []models.Dependency) (string, error) { if len(deps) == 0 { return "echo \"没有需要安装的依赖\"", nil diff --git a/internal/services/deps/parser.go b/internal/services/deps/parser.go new file mode 100644 index 0000000..199468b --- /dev/null +++ b/internal/services/deps/parser.go @@ -0,0 +1,105 @@ +package deps + +import ( + "encoding/json" + "regexp" + "strings" + + "github.com/engigu/baihu-panel/internal/models" +) + +// ParseManifest 根据语言解析依赖清单文件内容 +func ParseManifest(language, content string) ([]models.Dependency, error) { + lang := strings.ToLower(language) + if strings.Contains(lang, "python") { + return ParseRequirements(content), nil + } + if strings.Contains(lang, "node") { + return ParsePackageJson(content) + } + return []models.Dependency{}, nil +} + +// ParseRequirements 解析 Python requirements.txt +func ParseRequirements(content string) []models.Dependency { + var deps []models.Dependency + // 使用正则表达式按行分割,兼容 Windows 和 Linux 的换行符 + lines := regexp.MustCompile(`\r?\n`).Split(content, -1) + + // 用于分割包名和版本的正则 (支持 ==, >=, <=, ~=, >, <, @) + versionRegex := regexp.MustCompile(`[=><~@]+`) + + for _, line := range lines { + line = strings.TrimSpace(line) + // 忽略空行、注释行以及参数行 (以 - 开头的行如 -i, -r) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "-") { + continue + } + + // 分割名称与版本 + parts := versionRegex.Split(line, 2) + name := strings.TrimSpace(parts[0]) + version := "" + if len(parts) > 1 { + // 清除可能存在的后续参数,比如 requests==2.31.0 --hash=sha256:... + versionPart := strings.TrimSpace(parts[1]) + + // 如果有逗号分隔的多个范围限制,比如 >=1.20,<2.0,只取第一个范围作为参考版本号 + if idx := strings.Index(versionPart, ","); idx != -1 { + versionPart = versionPart[:idx] + } + + versionFields := strings.Fields(versionPart) + if len(versionFields) > 0 { + version = strings.TrimSpace(versionFields[0]) + // 清除可能残留的首部版本符号 + version = strings.TrimLeft(version, "=><~@ ") + } + } + + if name != "" { + deps = append(deps, models.Dependency{ + Name: name, + Version: version, + Language: "python3", + }) + } + } + return deps +} + +// PackageJson 代表 package.json 的结构定义 +type PackageJson struct { + Dependencies map[string]string `json:"dependencies"` + DevDependencies map[string]string `json:"devDependencies"` +} + +// ParsePackageJson 解析 Node.js package.json +func ParsePackageJson(content string) ([]models.Dependency, error) { + var pkg PackageJson + if err := json.Unmarshal([]byte(content), &pkg); err != nil { + return nil, err + } + + var deps []models.Dependency + collect := func(m map[string]string, isDev bool) { + for name, versionRange := range m { + // 移除 npm 常见版本范围修饰符(如 ^1.2.3 或 ~2.3.0,保留底线版本号) + version := strings.TrimLeft(versionRange, "^~>=<* ") + remark := "" + if isDev { + remark = "devDependencies" + } + deps = append(deps, models.Dependency{ + Name: name, + Version: version, + Language: "node", + Remark: remark, + }) + } + } + + collect(pkg.Dependencies, false) + collect(pkg.DevDependencies, true) + return deps, nil +} diff --git a/internal/services/deps/parser_test.go b/internal/services/deps/parser_test.go new file mode 100644 index 0000000..d584ebe --- /dev/null +++ b/internal/services/deps/parser_test.go @@ -0,0 +1,87 @@ +package deps + +import ( + "testing" +) + +func TestParseRequirements(t *testing.T) { + content := ` +# This is a comment +requests==2.31.0 +numpy>=1.20,<2.0 +gunicorn +-r other-requirements.txt + pandas ~= 1.3.0 +` + deps := ParseRequirements(content) + if len(deps) != 4 { + t.Fatalf("expected 4 dependencies, got %d", len(deps)) + } + + expected := []struct { + name string + version string + }{ + {"requests", "2.31.0"}, + {"numpy", "1.20"}, + {"gunicorn", ""}, + {"pandas", "1.3.0"}, + } + + for i, exp := range expected { + if deps[i].Name != exp.name { + t.Errorf("expected name %s, got %s", exp.name, deps[i].Name) + } + if deps[i].Version != exp.version { + t.Errorf("expected version %s, got %s", exp.version, deps[i].Version) + } + if deps[i].Language != "python3" { + t.Errorf("expected language python3, got %s", deps[i].Language) + } + } +} + +func TestParsePackageJson(t *testing.T) { + content := `{ + "dependencies": { + "lodash": "^4.17.21", + "express": "~4.18.2" + }, + "devDependencies": { + "typescript": "^5.0.4" + } +}` + deps, err := ParsePackageJson(content) + if err != nil { + t.Fatalf("failed to parse package.json: %v", err) + } + + if len(deps) != 3 { + t.Fatalf("expected 3 dependencies, got %d", len(deps)) + } + + expected := []struct { + name string + version string + remark string + }{ + {"lodash", "4.17.21", ""}, + {"express", "4.18.2", ""}, + {"typescript", "5.0.4", "devDependencies"}, + } + + for i, exp := range expected { + if deps[i].Name != exp.name { + t.Errorf("expected name %s, got %s", exp.name, deps[i].Name) + } + if deps[i].Version != exp.version { + t.Errorf("expected version %s, got %s", exp.version, deps[i].Version) + } + if deps[i].Remark != exp.remark { + t.Errorf("expected remark %s, got %s", exp.remark, deps[i].Remark) + } + if deps[i].Language != "node" { + t.Errorf("expected language node, got %s", deps[i].Language) + } + } +}