diff --git a/internal/controllers/runtime_controller.go b/internal/controllers/runtime_controller.go new file mode 100644 index 0000000..d3a2632 --- /dev/null +++ b/internal/controllers/runtime_controller.go @@ -0,0 +1,174 @@ +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.Param("type") + 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.Param("type") + 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.Param("type") + envName := c.Param("name") + + 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.Param("type") + envName := c.Param("name") + + 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.Param("type") + envName := c.Param("name") + + 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.Param("type") + envName := c.Param("name") + + 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/router/register.go b/internal/router/register.go index f8b4ea3..004c9bf 100644 --- a/internal/router/register.go +++ b/internal/router/register.go @@ -38,6 +38,7 @@ func RegisterControllers() *Controllers { Log: controllers.NewLogController(), Terminal: controllers.NewTerminalController(), Settings: controllers.NewSettingsController(userService, loginLogService), + Runtime: controllers.NewRuntimeController(), } } diff --git a/internal/router/router.go b/internal/router/router.go index 522b909..fae1e3d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -22,6 +22,7 @@ type Controllers struct { Log *controllers.LogController Terminal *controllers.TerminalController Settings *controllers.SettingsController + Runtime *controllers.RuntimeController } func mustSubFS(fsys fs.FS, dir string) fs.FS { @@ -177,6 +178,18 @@ func Setup(c *Controllers) *gin.Engine { settings.GET("/about", c.Settings.GetAbout) settings.GET("/loginlogs", c.Settings.GetLoginLogs) } + + // Runtime routes (依赖管理) + runtime := authorized.Group("/runtime") + { + runtime.GET("", c.Runtime.GetAvailableRuntimes) + runtime.GET("/:type/envs", c.Runtime.ListEnvs) + runtime.POST("/:type/envs", c.Runtime.CreateEnv) + runtime.DELETE("/:type/envs/:name", c.Runtime.DeleteEnv) + runtime.GET("/:type/envs/:name/packages", c.Runtime.ListPackages) + runtime.POST("/:type/envs/:name/packages", c.Runtime.InstallPackage) + runtime.DELETE("/:type/envs/:name/packages", c.Runtime.UninstallPackage) + } } } diff --git a/internal/services/deps_env/conda_service.go b/internal/services/deps_env/conda_service.go new file mode 100644 index 0000000..63fe876 --- /dev/null +++ b/internal/services/deps_env/conda_service.go @@ -0,0 +1,251 @@ +package deps_env + +import ( + "bufio" + "encoding/json" + "os/exec" + "strings" + + "baihu/internal/logger" +) + +// 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 +} + +// condaEnvJSON conda env list --json 的输出结构 +type condaEnvJSON struct { + Envs []string `json:"envs"` +} + +// 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 { + name := extractEnvName(envPath) + // 过滤以 . 开头的环境 + if strings.HasPrefix(name, ".") { + continue + } + envs = append(envs, RuntimeEnv{ + Name: name, + Path: envPath, + Active: false, + }) + } + + return envs, nil +} + +// extractEnvName 从路径中提取环境名称 +func extractEnvName(envPath string) string { + parts := strings.Split(envPath, "/") + if len(parts) > 0 { + name := parts[len(parts)-1] + // 如果是 base 环境,路径可能是 /opt/conda 这样的 + if name == "conda" || name == "miniconda3" || name == "anaconda3" { + return "base" + } + return name + } + return envPath +} + +// CreateEnv 创建 Conda 环境 +func (cm *CondaManager) CreateEnv(name string, version string) error { + condaPath := cm.getCondaPath() + if condaPath == "" { + return exec.ErrNotFound + } + + args := []string{"create", "-n", name, "-y"} + if version != "" { + args = append(args, "python="+version) + } + + 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 + } + + return nil +} + +// 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 new file mode 100644 index 0000000..253cea6 --- /dev/null +++ b/internal/services/deps_env/runtime_service.go @@ -0,0 +1,72 @@ +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 c68d601..f34100b 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -170,6 +170,20 @@ export const api = { } if (json.code !== 200) throw new Error(json.msg || '上传失败') } + }, + runtime: { + getAvailable: () => request('/runtime'), + listEnvs: (type: string) => request(`/runtime/${type}/envs`), + createEnv: (type: string, name: string, version?: string) => + request(`/runtime/${type}/envs`, { method: 'POST', body: JSON.stringify({ name, version }) }), + deleteEnv: (type: string, name: string) => + request(`/runtime/${type}/envs/${name}`, { method: 'DELETE' }), + listPackages: (type: string, envName: string) => + request(`/runtime/${type}/envs/${envName}/packages`), + installPackage: (type: string, envName: string, packageName: string) => + request(`/runtime/${type}/envs/${envName}/packages`, { method: 'POST', body: JSON.stringify({ package: packageName }) }), + uninstallPackage: (type: string, envName: string, packageName: string) => + request(`/runtime/${type}/envs/${envName}/packages`, { method: 'DELETE', body: JSON.stringify({ package: packageName }) }) } } @@ -305,3 +319,16 @@ export interface TaskStatsItem { task_name: string count: number } + +export interface RuntimeEnv { + name: string + path: string + version: string + active: boolean +} + +export interface RuntimePackage { + name: string + version: string + channel?: string +} diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index f041c02..abd2ff0 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -1,7 +1,7 @@ + +