From 07259ae00db4618ff59ca3f7e6f4f2f0be9f5a10 Mon Sep 17 00:00:00 2001 From: engigu Date: Tue, 23 Dec 2025 23:02:47 +0800 Subject: [PATCH] feat: add deps page --- internal/controllers/dependency_controller.go | 163 +++++++ internal/controllers/runtime_controller.go | 204 --------- internal/database/migrate.go | 1 + internal/models/dependency.go | 22 + internal/router/register.go | 22 +- internal/router/router.go | 39 +- internal/services/dependency_service.go | 177 ++++++++ internal/services/deps_env/conda_service.go | 302 ------------- internal/services/deps_env/runtime_service.go | 72 --- web/src/api/index.ts | 40 +- web/src/views/dependencies/Dependencies.vue | 422 ++++++------------ 11 files changed, 546 insertions(+), 918 deletions(-) create mode 100644 internal/controllers/dependency_controller.go delete mode 100644 internal/controllers/runtime_controller.go create mode 100644 internal/models/dependency.go create mode 100644 internal/services/dependency_service.go delete mode 100644 internal/services/deps_env/conda_service.go delete mode 100644 internal/services/deps_env/runtime_service.go diff --git a/internal/controllers/dependency_controller.go b/internal/controllers/dependency_controller.go new file mode 100644 index 0000000..d992798 --- /dev/null +++ b/internal/controllers/dependency_controller.go @@ -0,0 +1,163 @@ +package controllers + +import ( + "strconv" + + "baihu/internal/models" + "baihu/internal/services" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type DependencyController struct { + service *services.DependencyService +} + +func NewDependencyController() *DependencyController { + return &DependencyController{ + service: services.NewDependencyService(), + } +} + +// List 获取依赖列表 +func (c *DependencyController) List(ctx *gin.Context) { + depType := ctx.Query("type") + deps, err := c.service.List(depType) + if err != nil { + utils.ServerError(ctx, "获取依赖列表失败") + return + } + utils.Success(ctx, deps) +} + +// Create 添加依赖 +func (c *DependencyController) Create(ctx *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + Version string `json:"version"` + Type string `json:"type" binding:"required"` + Remark string `json:"remark"` + } + + if err := ctx.ShouldBindJSON(&req); err != nil { + utils.BadRequest(ctx, "参数错误") + return + } + + if req.Type != "py" && req.Type != "node" { + utils.BadRequest(ctx, "类型必须是 py 或 node") + return + } + + dep := &models.Dependency{ + Name: req.Name, + Version: req.Version, + Type: req.Type, + Remark: req.Remark, + } + + if err := c.service.Create(dep); err != nil { + utils.BadRequest(ctx, err.Error()) + return + } + + utils.Success(ctx, dep) +} + +// Delete 删除依赖 +func (c *DependencyController) Delete(ctx *gin.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + utils.BadRequest(ctx, "无效的 ID") + return + } + + if err := c.service.Delete(id); err != nil { + utils.ServerError(ctx, "删除失败") + return + } + + utils.SuccessMsg(ctx, "删除成功") +} + +// Install 安装依赖 +func (c *DependencyController) Install(ctx *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + Version string `json:"version"` + Type string `json:"type" binding:"required"` + } + + if err := ctx.ShouldBindJSON(&req); err != nil { + utils.BadRequest(ctx, "参数错误") + return + } + + dep := &models.Dependency{ + Name: req.Name, + Version: req.Version, + Type: req.Type, + } + + if err := c.service.Install(dep); err != nil { + utils.ServerError(ctx, err.Error()) + return + } + + // 安装成功后保存到数据库 + c.service.Create(dep) + + utils.SuccessMsg(ctx, "安装成功") +} + +// Uninstall 卸载依赖 +func (c *DependencyController) Uninstall(ctx *gin.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + utils.BadRequest(ctx, "无效的 ID") + return + } + + // 获取依赖信息 + deps, _ := c.service.List("") + var dep *models.Dependency + for _, d := range deps { + if d.ID == id { + dep = &d + break + } + } + + if dep == nil { + utils.NotFound(ctx, "依赖不存在") + return + } + + if err := c.service.Uninstall(dep); err != nil { + utils.ServerError(ctx, err.Error()) + return + } + + // 卸载成功后从数据库删除 + c.service.Delete(id) + + utils.SuccessMsg(ctx, "卸载成功") +} + +// GetInstalled 获取已安装的包 +func (c *DependencyController) GetInstalled(ctx *gin.Context) { + depType := ctx.Query("type") + if depType == "" { + utils.BadRequest(ctx, "缺少 type 参数") + return + } + + packages, err := c.service.GetInstalledPackages(depType) + if err != nil { + utils.ServerError(ctx, "获取已安装包失败: "+err.Error()) + return + } + + utils.Success(ctx, packages) +} diff --git a/internal/controllers/runtime_controller.go b/internal/controllers/runtime_controller.go deleted file mode 100644 index 002660c..0000000 --- a/internal/controllers/runtime_controller.go +++ /dev/null @@ -1,204 +0,0 @@ -package controllers - -import ( - "baihu/internal/services/deps_env" - "baihu/internal/utils" - - "github.com/gin-gonic/gin" -) - -type RuntimeController struct { - runtimeService *deps_env.RuntimeService -} - -func NewRuntimeController() *RuntimeController { - return &RuntimeController{ - runtimeService: deps_env.NewRuntimeService(), - } -} - -// GetAvailableRuntimes 获取可用的运行时列表 -func (rc *RuntimeController) GetAvailableRuntimes(c *gin.Context) { - runtimes := rc.runtimeService.GetAvailableRuntimes() - utils.Success(c, runtimes) -} - -// ListEnvs 列出指定运行时的所有环境 -func (rc *RuntimeController) ListEnvs(c *gin.Context) { - runtimeType := c.Query("type") - if runtimeType == "" { - utils.BadRequest(c, "缺少 type 参数") - return - } - - manager := rc.runtimeService.GetManager(runtimeType) - if manager == nil { - utils.NotFound(c, "运行时类型不存在") - return - } - - if !manager.IsAvailable() { - utils.BadRequest(c, "运行时不可用") - return - } - - envs, err := manager.ListEnvs() - if err != nil { - utils.ServerError(c, "获取环境列表失败: "+err.Error()) - return - } - - utils.Success(c, envs) -} - -// CreateEnv 创建环境 -func (rc *RuntimeController) CreateEnv(c *gin.Context) { - runtimeType := c.Query("type") - if runtimeType == "" { - utils.BadRequest(c, "缺少 type 参数") - return - } - - manager := rc.runtimeService.GetManager(runtimeType) - if manager == nil { - utils.NotFound(c, "运行时类型不存在") - return - } - - var req struct { - Name string `json:"name" binding:"required"` - Version string `json:"version"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - utils.BadRequest(c, "参数错误") - return - } - - if err := manager.CreateEnv(req.Name, req.Version); err != nil { - utils.ServerError(c, "创建环境失败: "+err.Error()) - return - } - - utils.SuccessMsg(c, "环境创建成功") -} - -// DeleteEnv 删除环境 -func (rc *RuntimeController) DeleteEnv(c *gin.Context) { - runtimeType := c.Query("type") - envName := c.Query("name") - - if runtimeType == "" || envName == "" { - utils.BadRequest(c, "缺少 type 或 name 参数") - return - } - - manager := rc.runtimeService.GetManager(runtimeType) - if manager == nil { - utils.NotFound(c, "运行时类型不存在") - return - } - - if envName == "base" { - utils.BadRequest(c, "不能删除 base 环境") - return - } - - if err := manager.DeleteEnv(envName); err != nil { - utils.ServerError(c, "删除环境失败: "+err.Error()) - return - } - - utils.SuccessMsg(c, "环境删除成功") -} - -// ListPackages 列出环境中的包 -func (rc *RuntimeController) ListPackages(c *gin.Context) { - runtimeType := c.Query("type") - envName := c.Query("env") - - if runtimeType == "" || envName == "" { - utils.BadRequest(c, "缺少 type 或 env 参数") - return - } - - manager := rc.runtimeService.GetManager(runtimeType) - if manager == nil { - utils.NotFound(c, "运行时类型不存在") - return - } - - packages, err := manager.ListPackages(envName) - if err != nil { - utils.ServerError(c, "获取包列表失败: "+err.Error()) - return - } - - utils.Success(c, packages) -} - -// InstallPackage 安装包 -func (rc *RuntimeController) InstallPackage(c *gin.Context) { - runtimeType := c.Query("type") - envName := c.Query("env") - - if runtimeType == "" || envName == "" { - utils.BadRequest(c, "缺少 type 或 env 参数") - return - } - - manager := rc.runtimeService.GetManager(runtimeType) - if manager == nil { - utils.NotFound(c, "运行时类型不存在") - return - } - - var req struct { - Package string `json:"package" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - utils.BadRequest(c, "参数错误") - return - } - - if err := manager.InstallPackage(envName, req.Package); err != nil { - utils.ServerError(c, "安装包失败: "+err.Error()) - return - } - - utils.SuccessMsg(c, "包安装成功") -} - -// UninstallPackage 卸载包 -func (rc *RuntimeController) UninstallPackage(c *gin.Context) { - runtimeType := c.Query("type") - envName := c.Query("env") - - if runtimeType == "" || envName == "" { - utils.BadRequest(c, "缺少 type 或 env 参数") - return - } - - manager := rc.runtimeService.GetManager(runtimeType) - if manager == nil { - utils.NotFound(c, "运行时类型不存在") - return - } - - var req struct { - Package string `json:"package" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - utils.BadRequest(c, "参数错误") - return - } - - if err := manager.UninstallPackage(envName, req.Package); err != nil { - utils.ServerError(c, "卸载包失败: "+err.Error()) - return - } - - utils.SuccessMsg(c, "包卸载成功") -} diff --git a/internal/database/migrate.go b/internal/database/migrate.go index b4d3768..41e351f 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -14,5 +14,6 @@ func Migrate() error { &models.Setting{}, &models.LoginLog{}, &models.SendStats{}, + &models.Dependency{}, ) } diff --git a/internal/models/dependency.go b/internal/models/dependency.go new file mode 100644 index 0000000..b86031b --- /dev/null +++ b/internal/models/dependency.go @@ -0,0 +1,22 @@ +package models + +import ( + "baihu/internal/constant" + "time" +) + +// Dependency 依赖包模型 +type Dependency struct { + ID int `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:100;not null"` + Version string `json:"version" gorm:"size:50"` + Type string `json:"type" gorm:"size:10;not null"` // py 或 node + Remark string `json:"remark" gorm:"size:255"` + Log string `json:"log" gorm:"type:text"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (Dependency) TableName() string { + return constant.TablePrefix + "deps" +} diff --git a/internal/router/register.go b/internal/router/register.go index 30e710f..3a0c0a7 100644 --- a/internal/router/register.go +++ b/internal/router/register.go @@ -28,17 +28,17 @@ func RegisterControllers() *Controllers { // Initialize and return controllers return &Controllers{ - Task: controllers.NewTaskController(taskService, cronService), - Auth: controllers.NewAuthController(userService, settingsService, loginLogService), - Env: controllers.NewEnvController(envService), - Script: controllers.NewScriptController(scriptService), - Executor: controllers.NewExecutorController(executorService), - File: controllers.NewFileController(constant.ScriptsWorkDir), - Dashboard: controllers.NewDashboardController(cronService, executorService), - Log: controllers.NewLogController(), - Terminal: controllers.NewTerminalController(), - Settings: controllers.NewSettingsController(userService, loginLogService, executorService), - Runtime: controllers.NewRuntimeController(), + Task: controllers.NewTaskController(taskService, cronService), + Auth: controllers.NewAuthController(userService, settingsService, loginLogService), + Env: controllers.NewEnvController(envService), + Script: controllers.NewScriptController(scriptService), + Executor: controllers.NewExecutorController(executorService), + File: controllers.NewFileController(constant.ScriptsWorkDir), + Dashboard: controllers.NewDashboardController(cronService, executorService), + Log: controllers.NewLogController(), + Terminal: controllers.NewTerminalController(), + Settings: controllers.NewSettingsController(userService, loginLogService, executorService), + Dependency: controllers.NewDependencyController(), } } diff --git a/internal/router/router.go b/internal/router/router.go index b8722d4..22bbe7e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -12,17 +12,17 @@ import ( ) type Controllers struct { - Task *controllers.TaskController - Auth *controllers.AuthController - Env *controllers.EnvController - Script *controllers.ScriptController - Executor *controllers.ExecutorController - File *controllers.FileController - Dashboard *controllers.DashboardController - Log *controllers.LogController - Terminal *controllers.TerminalController - Settings *controllers.SettingsController - Runtime *controllers.RuntimeController + Task *controllers.TaskController + Auth *controllers.AuthController + Env *controllers.EnvController + Script *controllers.ScriptController + Executor *controllers.ExecutorController + File *controllers.FileController + Dashboard *controllers.DashboardController + Log *controllers.LogController + Terminal *controllers.TerminalController + Settings *controllers.SettingsController + Dependency *controllers.DependencyController } func mustSubFS(fsys fs.FS, dir string) fs.FS { @@ -185,16 +185,15 @@ func Setup(c *Controllers) *gin.Engine { settings.POST("/restore", c.Settings.RestoreBackup) } - // Runtime routes (依赖管理) - runtime := authorized.Group("/runtime") + // Dependency routes (依赖管理) + deps := authorized.Group("/deps") { - runtime.GET("", c.Runtime.GetAvailableRuntimes) - runtime.GET("/envs", c.Runtime.ListEnvs) - runtime.POST("/envs", c.Runtime.CreateEnv) - runtime.DELETE("/envs", c.Runtime.DeleteEnv) - runtime.GET("/packages", c.Runtime.ListPackages) - runtime.POST("/packages", c.Runtime.InstallPackage) - runtime.DELETE("/packages", c.Runtime.UninstallPackage) + deps.GET("", c.Dependency.List) + deps.POST("", c.Dependency.Create) + deps.DELETE("/:id", c.Dependency.Delete) + deps.POST("/install", c.Dependency.Install) + deps.POST("/uninstall/:id", c.Dependency.Uninstall) + deps.GET("/installed", c.Dependency.GetInstalled) } } } diff --git a/internal/services/dependency_service.go b/internal/services/dependency_service.go new file mode 100644 index 0000000..4a899d7 --- /dev/null +++ b/internal/services/dependency_service.go @@ -0,0 +1,177 @@ +package services + +import ( + "errors" + "os/exec" + "strings" + + "baihu/internal/database" + "baihu/internal/logger" + "baihu/internal/models" +) + +type DependencyService struct{} + +func NewDependencyService() *DependencyService { + return &DependencyService{} +} + +// List 获取依赖列表 +func (s *DependencyService) List(depType string) ([]models.Dependency, error) { + var deps []models.Dependency + query := database.DB + if depType != "" { + query = query.Where("type = ?", depType) + } + err := query.Order("id desc").Find(&deps).Error + return deps, err +} + +// Create 创建依赖记录 +func (s *DependencyService) Create(dep *models.Dependency) error { + // 检查是否已存在 + var existing models.Dependency + if err := database.DB.Where("name = ? AND type = ?", dep.Name, dep.Type).First(&existing).Error; err == nil { + return errors.New("依赖已存在") + } + return database.DB.Create(dep).Error +} + +// Delete 删除依赖记录 +func (s *DependencyService) Delete(id int) error { + return database.DB.Delete(&models.Dependency{}, id).Error +} + +// Install 安装依赖 +func (s *DependencyService) Install(dep *models.Dependency) error { + var cmd *exec.Cmd + var packageSpec string + + if dep.Version != "" { + if dep.Type == "py" { + packageSpec = dep.Name + "==" + dep.Version + } else { + packageSpec = dep.Name + "@" + dep.Version + } + } else { + packageSpec = dep.Name + } + + switch dep.Type { + case "py": + cmd = exec.Command("pip", "install", packageSpec) + case "node": + cmd = exec.Command("npm", "install", "-g", packageSpec) + default: + return errors.New("不支持的依赖类型") + } + + logger.Infof("Installing %s package: %s", dep.Type, packageSpec) + output, err := cmd.CombinedOutput() + dep.Log = string(output) + + if err != nil { + logger.Errorf("Install failed: %v, output: %s", err, string(output)) + return errors.New("安装失败: " + string(output)) + } + logger.Infof("Install success: %s", packageSpec) + + return nil +} + +// Uninstall 卸载依赖 +func (s *DependencyService) Uninstall(dep *models.Dependency) error { + var cmd *exec.Cmd + + switch dep.Type { + case "py": + cmd = exec.Command("pip", "uninstall", "-y", dep.Name) + case "node": + cmd = exec.Command("npm", "uninstall", "-g", dep.Name) + default: + return errors.New("不支持的依赖类型") + } + + logger.Infof("Uninstalling %s package: %s", dep.Type, dep.Name) + output, err := cmd.CombinedOutput() + if err != nil { + logger.Errorf("Uninstall failed: %v, output: %s", err, string(output)) + return errors.New("卸载失败: " + string(output)) + } + + return nil +} + +// GetInstalledPackages 获取已安装的包列表 +func (s *DependencyService) GetInstalledPackages(depType string) ([]models.Dependency, error) { + var packages []models.Dependency + + switch depType { + case "py": + return s.getPipPackages() + case "node": + return s.getNpmPackages() + default: + return packages, errors.New("不支持的依赖类型") + } +} + +// getPipPackages 获取 pip 已安装的包 +func (s *DependencyService) getPipPackages() ([]models.Dependency, error) { + cmd := exec.Command("pip", "list", "--format=freeze") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + var packages []models.Dependency + lines := strings.Split(string(output), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.SplitN(line, "==", 2) + pkg := models.Dependency{ + Name: parts[0], + Type: "py", + } + if len(parts) > 1 { + pkg.Version = parts[1] + } + packages = append(packages, pkg) + } + + return packages, nil +} + +// getNpmPackages 获取 npm 全局安装的包 +func (s *DependencyService) getNpmPackages() ([]models.Dependency, error) { + cmd := exec.Command("npm", "list", "-g", "--depth=0", "--json") + output, err := cmd.Output() + if err != nil { + // npm list 在没有包时也会返回错误,忽略 + } + + var packages []models.Dependency + // 简单解析,不用 json 库 + lines := strings.Split(string(output), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.Contains(line, `"version"`) { + continue + } + if strings.HasPrefix(line, `"`) && strings.Contains(line, ":") { + // 格式: "package-name": { + name := strings.Trim(strings.Split(line, ":")[0], `" `) + if name != "" && name != "dependencies" { + packages = append(packages, models.Dependency{ + Name: name, + Type: "node", + }) + } + } + } + + return packages, nil +} diff --git a/internal/services/deps_env/conda_service.go b/internal/services/deps_env/conda_service.go deleted file mode 100644 index 85b7639..0000000 --- a/internal/services/deps_env/conda_service.go +++ /dev/null @@ -1,302 +0,0 @@ -package deps_env - -import ( - "bufio" - "encoding/json" - "os" - "os/exec" - "path/filepath" - "strings" - - "baihu/internal/constant" - "baihu/internal/logger" -) - -// getEnvsDir 获取虚拟环境存储目录 -func getEnvsDir() string { - return filepath.Join(constant.DataDir, "envs") -} - -// CondaManager Conda 运行时管理器 -type CondaManager struct { - condaPath string -} - -// NewCondaManager 创建 Conda 管理器 -func NewCondaManager() *CondaManager { - return &CondaManager{} -} - -// GetType 获取运行时类型 -func (cm *CondaManager) GetType() string { - return "conda" -} - -// IsAvailable 检查 Conda 是否可用 -func (cm *CondaManager) IsAvailable() bool { - path, err := cm.findCondaPath() - if err != nil { - return false - } - cm.condaPath = path - return true -} - -// findCondaPath 查找 conda 可执行文件路径 -func (cm *CondaManager) findCondaPath() (string, error) { - // 尝试常见的 conda 路径 - paths := []string{"conda", "micromamba", "/opt/conda/bin/conda", "/root/miniconda3/bin/conda", "/root/anaconda3/bin/conda"} - for _, p := range paths { - if path, err := exec.LookPath(p); err == nil { - return path, nil - } - } - return "", exec.ErrNotFound -} - -// getCondaPath 获取 conda 路径 -func (cm *CondaManager) getCondaPath() string { - if cm.condaPath == "" { - cm.findCondaPath() - } - return cm.condaPath -} - -// condaEnvDetail 环境详情 -type condaEnvDetail struct { - Name string `json:"name"` - Active bool `json:"active"` -} - -// condaEnvJSON conda env list --json 的输出结构 -type condaEnvJSON struct { - Envs []string `json:"envs"` - EnvsDetails map[string]condaEnvDetail `json:"envs_details"` -} - -// ListEnvs 列出所有 Conda 环境 -func (cm *CondaManager) ListEnvs() ([]RuntimeEnv, error) { - condaPath := cm.getCondaPath() - if condaPath == "" { - return nil, exec.ErrNotFound - } - - cmd := exec.Command(condaPath, "env", "list", "--json") - output, err := cmd.Output() - if err != nil { - logger.Errorf("Failed to list conda envs: %v", err) - return nil, err - } - - var envJSON condaEnvJSON - if err := json.Unmarshal(output, &envJSON); err != nil { - return nil, err - } - - var envs []RuntimeEnv - for _, envPath := range envJSON.Envs { - detail, ok := envJSON.EnvsDetails[envPath] - name := "" - active := false - if ok { - name = detail.Name - active = detail.Active - } - // 如果没有 name,使用路径 - if name == "" { - name = envPath - } - envs = append(envs, RuntimeEnv{ - Name: name, - Path: envPath, - Active: active, - }) - } - - return envs, nil -} - -// CreateEnv 创建 Conda 环境 -func (cm *CondaManager) CreateEnv(name string, version string) error { - condaPath := cm.getCondaPath() - if condaPath == "" { - return exec.ErrNotFound - } - - envsDir := getEnvsDir() - // 确保目录存在 - if err := os.MkdirAll(envsDir, 0755); err != nil { - return err - } - - envPath := filepath.Join(envsDir, name) - args := []string{"create", "-p", envPath, "-y"} - if version != "" { - args = append(args, "python="+version) - } - - logger.Infof("Creating conda env: %s %v", condaPath, args) - cmd := exec.Command(condaPath, args...) - output, err := cmd.CombinedOutput() - if err != nil { - logger.Errorf("Failed to create conda env: %v, output: %s", err, string(output)) - return err - } - logger.Infof("Conda env created: %s", envPath) - - // 写入 environments.txt - if err := cm.appendToEnvironmentsTxt(envPath); err != nil { - logger.Errorf("Failed to write environments.txt: %v", err) - } - - return nil -} - -// appendToEnvironmentsTxt 将环境路径追加到 environments.txt -func (cm *CondaManager) appendToEnvironmentsTxt(envPath string) error { - absPath, err := filepath.Abs(envPath) - if err != nil { - return err - } - - envsDir := getEnvsDir() - envsTxtPath := filepath.Join(envsDir, "environments.txt") - - // 读取现有内容,检查是否已存在 - content, _ := os.ReadFile(envsTxtPath) - lines := strings.Split(string(content), "\n") - for _, line := range lines { - if strings.TrimSpace(line) == absPath { - return nil // 已存在 - } - } - - // 追加写入 - f, err := os.OpenFile(envsTxtPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(absPath + "\n") - return err -} - -// DeleteEnv 删除 Conda 环境 -func (cm *CondaManager) DeleteEnv(name string) error { - condaPath := cm.getCondaPath() - if condaPath == "" { - return exec.ErrNotFound - } - - if name == "base" { - return nil // 不允许删除 base 环境 - } - - cmd := exec.Command(condaPath, "env", "remove", "-n", name, "-y") - output, err := cmd.CombinedOutput() - if err != nil { - logger.Errorf("Failed to delete conda env: %v, output: %s", err, string(output)) - return err - } - - return nil -} - -// ListPackages 列出环境中的包 -func (cm *CondaManager) ListPackages(envName string) ([]RuntimePackage, error) { - condaPath := cm.getCondaPath() - if condaPath == "" { - return nil, exec.ErrNotFound - } - - args := []string{"list"} - if envName != "" && envName != "base" { - args = append(args, "-n", envName) - } - - cmd := exec.Command(condaPath, args...) - output, err := cmd.Output() - if err != nil { - logger.Errorf("Failed to list packages: %v", err) - return nil, err - } - - return parseCondaList(string(output)), nil -} - -// parseCondaList 解析 conda list 输出 -func parseCondaList(output string) []RuntimePackage { - var packages []RuntimePackage - scanner := bufio.NewScanner(strings.NewReader(output)) - - for scanner.Scan() { - line := scanner.Text() - // 跳过注释和空行 - if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" { - continue - } - - fields := strings.Fields(line) - if len(fields) >= 2 { - name := fields[0] - - pkg := RuntimePackage{ - Name: name, - Version: fields[1], - } - if len(fields) >= 4 { - pkg.Channel = fields[3] - } - packages = append(packages, pkg) - } - } - - return packages -} - -// InstallPackage 安装包 -func (cm *CondaManager) InstallPackage(envName string, packageName string) error { - condaPath := cm.getCondaPath() - if condaPath == "" { - return exec.ErrNotFound - } - - args := []string{"install", "-y"} - if envName != "" && envName != "base" { - args = append(args, "-n", envName) - } - args = append(args, packageName) - - cmd := exec.Command(condaPath, args...) - output, err := cmd.CombinedOutput() - if err != nil { - logger.Errorf("Failed to install package: %v, output: %s", err, string(output)) - return err - } - - return nil -} - -// UninstallPackage 卸载包 -func (cm *CondaManager) UninstallPackage(envName string, packageName string) error { - condaPath := cm.getCondaPath() - if condaPath == "" { - return exec.ErrNotFound - } - - args := []string{"remove", "-y"} - if envName != "" && envName != "base" { - args = append(args, "-n", envName) - } - args = append(args, packageName) - - cmd := exec.Command(condaPath, args...) - output, err := cmd.CombinedOutput() - if err != nil { - logger.Errorf("Failed to uninstall package: %v, output: %s", err, string(output)) - return err - } - - return nil -} diff --git a/internal/services/deps_env/runtime_service.go b/internal/services/deps_env/runtime_service.go deleted file mode 100644 index 253cea6..0000000 --- a/internal/services/deps_env/runtime_service.go +++ /dev/null @@ -1,72 +0,0 @@ -package deps_env - -// RuntimeEnv 运行时环境信息 -type RuntimeEnv struct { - Name string `json:"name"` - Path string `json:"path"` - Version string `json:"version"` - Active bool `json:"active"` -} - -// RuntimePackage 包信息 -type RuntimePackage struct { - Name string `json:"name"` - Version string `json:"version"` - Channel string `json:"channel,omitempty"` -} - -// RuntimeManager 运行时管理器接口 -type RuntimeManager interface { - // GetType 获取运行时类型 - GetType() string - // IsAvailable 检查运行时是否可用 - IsAvailable() bool - // ListEnvs 列出所有环境 - ListEnvs() ([]RuntimeEnv, error) - // CreateEnv 创建环境 - CreateEnv(name string, version string) error - // DeleteEnv 删除环境 - DeleteEnv(name string) error - // ListPackages 列出环境中的包 - ListPackages(envName string) ([]RuntimePackage, error) - // InstallPackage 安装包 - InstallPackage(envName string, packageName string) error - // UninstallPackage 卸载包 - UninstallPackage(envName string, packageName string) error -} - -// RuntimeService 运行时服务 -type RuntimeService struct { - managers map[string]RuntimeManager -} - -// NewRuntimeService 创建运行时服务 -func NewRuntimeService() *RuntimeService { - rs := &RuntimeService{ - managers: make(map[string]RuntimeManager), - } - // 注册 Conda 管理器 - rs.RegisterManager(NewCondaManager()) - return rs -} - -// RegisterManager 注册运行时管理器 -func (rs *RuntimeService) RegisterManager(manager RuntimeManager) { - rs.managers[manager.GetType()] = manager -} - -// GetManager 获取指定类型的管理器 -func (rs *RuntimeService) GetManager(runtimeType string) RuntimeManager { - return rs.managers[runtimeType] -} - -// GetAvailableRuntimes 获取可用的运行时列表 -func (rs *RuntimeService) GetAvailableRuntimes() []string { - var available []string - for name, manager := range rs.managers { - if manager.IsAvailable() { - available = append(available, name) - } - } - return available -} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index ea64b8c..752172f 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -192,19 +192,18 @@ export const api = { if (json.code !== 200) throw new Error(json.msg || '上传失败') } }, - runtime: { - getAvailable: () => request('/runtime'), - listEnvs: (type: string) => request(`/runtime/envs?type=${type}`), - createEnv: (type: string, name: string, version?: string) => - request(`/runtime/envs?type=${type}`, { method: 'POST', body: JSON.stringify({ name, version }) }), - deleteEnv: (type: string, name: string) => - request(`/runtime/envs?type=${type}&name=${encodeURIComponent(name)}`, { method: 'DELETE' }), - listPackages: (type: string, envName: string) => - request(`/runtime/packages?type=${type}&env=${encodeURIComponent(envName)}`), - installPackage: (type: string, envName: string, packageName: string) => - request(`/runtime/packages?type=${type}&env=${encodeURIComponent(envName)}`, { method: 'POST', body: JSON.stringify({ package: packageName }) }), - uninstallPackage: (type: string, envName: string, packageName: string) => - request(`/runtime/packages?type=${type}&env=${encodeURIComponent(envName)}`, { method: 'DELETE', body: JSON.stringify({ package: packageName }) }) + deps: { + list: (type?: string) => { + const query = type ? `?type=${type}` : '' + return request(`/deps${query}`) + }, + create: (data: { name: string; version?: string; type: string; remark?: string }) => + request('/deps', { method: 'POST', body: JSON.stringify(data) }), + delete: (id: number) => request(`/deps/${id}`, { method: 'DELETE' }), + install: (data: { name: string; version?: string; type: string }) => + request('/deps/install', { method: 'POST', body: JSON.stringify(data) }), + uninstall: (id: number) => request(`/deps/uninstall/${id}`, { method: 'POST' }), + getInstalled: (type: string) => request(`/deps/installed?type=${type}`) } } @@ -347,15 +346,12 @@ export interface TaskStatsItem { count: number } -export interface RuntimeEnv { - name: string - path: string - version: string - active: boolean -} - -export interface RuntimePackage { +export interface Dependency { + id: number name: string version: string - channel?: string + type: string + remark: string + created_at: string + updated_at: string } diff --git a/web/src/views/dependencies/Dependencies.vue b/web/src/views/dependencies/Dependencies.vue index f2d8574..508a958 100644 --- a/web/src/views/dependencies/Dependencies.vue +++ b/web/src/views/dependencies/Dependencies.vue @@ -7,152 +7,66 @@ import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { Plus, Trash2, Package, Search, RefreshCw, Loader2 } from 'lucide-vue-next' -import { api, type RuntimeEnv, type RuntimePackage } from '@/api' +import { Plus, Trash2, Package, Search, RefreshCw, Loader2, Download } from 'lucide-vue-next' +import { api, type Dependency } from '@/api' import { toast } from 'vue-sonner' -const availableRuntimes = ref([]) -const activeRuntime = ref('conda') -const envs = ref([]) -const selectedEnv = ref(null) -const packages = ref([]) +const activeTab = ref('py') +const deps = ref([]) const loading = ref(false) -const packagesLoading = ref(false) - -// 创建环境 -const showCreateDialog = ref(false) -const newEnvName = ref('') -const newEnvVersion = ref('') -const creating = ref(false) - -// 删除环境 -const showDeleteDialog = ref(false) -const envToDelete = ref(null) - -// 安装包 -const showInstallDialog = ref(false) -const packageToInstall = ref('') const installing = ref(false) -// 搜索 -const packageSearch = ref('') +// 安装对话框 +const showInstallDialog = ref(false) +const newPkgName = ref('') +const newPkgVersion = ref('') -const filteredPackages = computed(() => { - if (!packageSearch.value) return packages.value - const q = packageSearch.value.toLowerCase() - return packages.value.filter(p => p.name.toLowerCase().includes(q)) +// 删除确认 +const showDeleteDialog = ref(false) +const depToDelete = ref(null) + +// 搜索 +const searchQuery = ref('') + +const filteredDeps = computed(() => { + const list = deps.value.filter(d => d.type === activeTab.value) + if (!searchQuery.value) return list + const q = searchQuery.value.toLowerCase() + return list.filter(d => d.name.toLowerCase().includes(q)) }) -async function loadRuntimes() { - try { - availableRuntimes.value = await api.runtime.getAvailable() - if (availableRuntimes.value.length > 0 && !availableRuntimes.value.includes(activeRuntime.value)) { - activeRuntime.value = availableRuntimes.value[0] ?? 'conda' - } - } catch { - availableRuntimes.value = [] - } -} - -async function loadEnvs() { - if (!activeRuntime.value) return +async function loadDeps() { loading.value = true try { - envs.value = await api.runtime.listEnvs(activeRuntime.value) - if (envs.value.length > 0 && !selectedEnv.value) { - const firstEnv = envs.value[0] - if (firstEnv) selectEnv(firstEnv) - } + deps.value = await api.deps.list() } catch { - toast.error('加载环境列表失败') - envs.value = [] + toast.error('加载依赖列表失败') } finally { loading.value = false } } -async function selectEnv(env: RuntimeEnv) { - selectedEnv.value = env - await loadPackages() -} - -async function loadPackages() { - if (!selectedEnv.value) return - packagesLoading.value = true - try { - packages.value = await api.runtime.listPackages(activeRuntime.value, selectedEnv.value.name) - } catch { - toast.error('加载包列表失败') - packages.value = [] - } finally { - packagesLoading.value = false - } -} - -function openCreateDialog() { - newEnvName.value = '' - newEnvVersion.value = '' - showCreateDialog.value = true -} - -async function createEnv() { - if (!newEnvName.value.trim()) { - toast.error('请输入环境名称') - return - } - creating.value = true - try { - await api.runtime.createEnv(activeRuntime.value, newEnvName.value.trim(), newEnvVersion.value.trim() || undefined) - toast.success('环境创建成功') - showCreateDialog.value = false - await loadEnvs() - } catch (e: unknown) { - toast.error((e as Error).message || '创建失败') - } finally { - creating.value = false - } -} - -function confirmDeleteEnv(env: RuntimeEnv) { - if (env.name === 'base') { - toast.error('不能删除 base 环境') - return - } - envToDelete.value = env - showDeleteDialog.value = true -} - -async function deleteEnv() { - if (!envToDelete.value) return - try { - await api.runtime.deleteEnv(activeRuntime.value, envToDelete.value.name) - toast.success('环境删除成功') - if (selectedEnv.value?.name === envToDelete.value.name) { - selectedEnv.value = null - packages.value = [] - } - await loadEnvs() - } catch (e: unknown) { - toast.error((e as Error).message || '删除失败') - } finally { - showDeleteDialog.value = false - envToDelete.value = null - } -} - function openInstallDialog() { - packageToInstall.value = '' + newPkgName.value = '' + newPkgVersion.value = '' showInstallDialog.value = true } async function installPackage() { - if (!packageToInstall.value.trim() || !selectedEnv.value) return + if (!newPkgName.value.trim()) { + toast.error('请输入包名') + return + } installing.value = true try { - await api.runtime.installPackage(activeRuntime.value, selectedEnv.value.name, packageToInstall.value.trim()) - toast.success('包安装成功') + await api.deps.install({ + name: newPkgName.value.trim(), + version: newPkgVersion.value.trim() || undefined, + type: activeTab.value + }) + toast.success('安装成功') showInstallDialog.value = false - await loadPackages() + await loadDeps() } catch (e: unknown) { toast.error((e as Error).message || '安装失败') } finally { @@ -160,29 +74,30 @@ async function installPackage() { } } -async function uninstallPackage(pkg: RuntimePackage) { - if (!selectedEnv.value) return +function confirmDelete(dep: Dependency) { + depToDelete.value = dep + showDeleteDialog.value = true +} + +async function uninstallPackage() { + if (!depToDelete.value) return try { - await api.runtime.uninstallPackage(activeRuntime.value, selectedEnv.value.name, pkg.name) - toast.success('包卸载成功') - await loadPackages() + await api.deps.uninstall(depToDelete.value.id) + toast.success('卸载成功') + await loadDeps() } catch (e: unknown) { toast.error((e as Error).message || '卸载失败') + } finally { + showDeleteDialog.value = false + depToDelete.value = null } } -function getRuntimeLabel(type: string) { - const labels: Record = { conda: 'Conda', node: 'Node.js' } - return labels[type] || type +function getTypeLabel(type: string) { + return type === 'py' ? 'Python' : 'Node.js' } -onMounted(async () => { - await loadRuntimes() - if (availableRuntimes.value.includes('conda')) { - activeRuntime.value = 'conda' - await loadEnvs() - } -}) +onMounted(loadDeps)