diff --git a/internal/controllers/mise_controller.go b/internal/controllers/mise_controller.go index 0aa9c67..8c79919 100644 --- a/internal/controllers/mise_controller.go +++ b/internal/controllers/mise_controller.go @@ -60,3 +60,19 @@ func (c *MiseController) Versions(ctx *gin.Context) { } utils.Success(ctx, versions) } + +// VerifyCommand 获取验证命令 +func (c *MiseController) VerifyCommand(ctx *gin.Context) { + plugin := ctx.Query("plugin") + version := ctx.Query("version") + if plugin == "" { + utils.BadRequest(ctx, "参数 plugin 不能为空") + return + } + cmd, err := c.service.GetVerifyCommand(plugin, version) + if err != nil { + utils.ServerError(ctx, "获取验证命令失败: "+err.Error()) + return + } + utils.Success(ctx, gin.H{"command": cmd}) +} diff --git a/internal/executor/executor.go b/internal/executor/executor.go index d843f2c..56732d8 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -76,6 +76,20 @@ func Execute(ctx context.Context, req Request, stdout, stderr io.Writer) (*Resul func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer, hooks Hooks) (*Result, error) { start := time.Now() + // 2. 执行命令 + timeout := req.Timeout + if timeout <= 0 { + timeout = 30 + } + execCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Minute) + defer cancel() + + // 如果指定使用 mise,则预先构建好带 mise 的命令,这样 PreExecute 记录的就是完整命令 + if req.UseMise { + req.Command = utils.BuildMiseCommand(req.Command, req.Languages) + req.UseMise = false + } + // 1. 执行前钩子 var logID uint if hooks != nil { @@ -92,19 +106,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer logID = id } - // 2. 执行命令 - timeout := req.Timeout - if timeout <= 0 { - timeout = 30 - } - execCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Minute) - defer cancel() - - finalCommand := req.Command - if req.UseMise { - finalCommand = BuildLanguageCommand(req.Command, req.Languages) - } - shell, args := utils.GetShellCommand(finalCommand) + shell, args := utils.GetShellCommand(req.Command) cmd := exec.CommandContext(execCtx, shell, args...) // 设置工作目录 @@ -280,31 +282,6 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer return result, err } -// BuildLanguageCommand 构建语言环境执行命令 (使用 mise) -func BuildLanguageCommand(command string, languages []map[string]string) string { - if len(languages) == 0 { - return command - } - - var builder strings.Builder - builder.WriteString("mise exec") - - for _, lang := range languages { - name := lang["name"] - version := lang["version"] - if name == "" { - continue - } - if version == "" { - version = "latest" - } - builder.WriteString(" " + name + "@" + version) - } - - builder.WriteString(" -- " + command) - return builder.String() -} - // ParseEnvVars 解析环境变量字符串 "KEY1=VALUE1,KEY2=VALUE2" func ParseEnvVars(envStr string) []string { if envStr == "" { diff --git a/internal/executor/scheduler.go b/internal/executor/scheduler.go index 9d483c3..bbc5e25 100644 --- a/internal/executor/scheduler.go +++ b/internal/executor/scheduler.go @@ -10,6 +10,7 @@ import ( "time" "github.com/engigu/baihu-panel/internal/constant" + "github.com/engigu/baihu-panel/internal/utils" ) // safeBuffer 一个线程安全的字节缓冲区,用于合并 stdout 和 stderr @@ -313,11 +314,12 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error) s.logger.Infof("[Scheduler] 执行任务 %s (名称: %s, 类型: %s)", req.TaskID, req.Name, req.Type) - finalCommand := req.Command + // 如果指定使用 mise,则预先构建好带 mise 的命令,这样 OnTaskExecuting 记录的就是完整命令 if req.UseMise { - finalCommand = BuildLanguageCommand(req.Command, req.Languages) + req.Command = utils.BuildMiseCommand(req.Command, req.Languages) + req.UseMise = false } - s.logger.Infof("[Scheduler] 实际执行命令: %s", finalCommand) + s.logger.Infof("[Scheduler] 实际执行命令: %s", req.Command) if s.config.Verbose { workDir := req.WorkDir diff --git a/internal/router/router.go b/internal/router/router.go index 54fc59f..6246a85 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -233,6 +233,7 @@ func Setup(c *Controllers) *gin.Engine { mise.POST("/sync", c.Mise.Sync) mise.GET("/plugins", c.Mise.Plugins) mise.GET("/versions", c.Mise.Versions) + mise.GET("/verify-cmd", c.Mise.VerifyCommand) } // Agent API(供前端调用,保持在 v1 下) diff --git a/internal/services/dependency_service.go b/internal/services/dependency_service.go index 7186f85..fc4e164 100644 --- a/internal/services/dependency_service.go +++ b/internal/services/dependency_service.go @@ -94,3 +94,12 @@ func (s *DependencyService) GetReinstallAllCommand(language, langVersion string) return m.GetReinstallAllCommand(deps_list) } + +// GetVerifyCommand 获取环境验证命令 +func (s *DependencyService) GetVerifyCommand(language, langVersion string) (string, error) { + m := deps.GetManager(language) + if m == nil { + return "", errors.New("不支持的依赖类型: " + language) + } + return m.GetVerifyCommand(langVersion) +} diff --git a/internal/services/deps/elixir.go b/internal/services/deps/elixir.go index ede1cc8..54e60fb 100644 --- a/internal/services/deps/elixir.go +++ b/internal/services/deps/elixir.go @@ -12,12 +12,17 @@ type ElixirManager struct { } func NewElixirManager(language string) *ElixirManager { + verifyCmd := []string{"elixir", "-v"} + if strings.Contains(strings.ToLower(language), "erlang") { + verifyCmd = []string{"erl", "+V"} + } return &ElixirManager{ BaseManager: BaseManager{ Language: language, InstallCmd: []string{"mix", "archive.install", "hex", "--force"}, UninstallCmd: []string{"mix", "archive.uninstall"}, ListCmd: []string{"mix", "archive"}, + VerifyCmd: verifyCmd, Separator: " ", }, } diff --git a/internal/services/deps/go.go b/internal/services/deps/go.go index fdeecbf..bf8ac3d 100644 --- a/internal/services/deps/go.go +++ b/internal/services/deps/go.go @@ -18,6 +18,7 @@ func NewGoManager(language string) *GoManager { InstallCmd: []string{"go", "install"}, UninstallCmd: []string{"go", "clean", "-i"}, ListCmd: []string{"go", "list", "..."}, + VerifyCmd: []string{"go", "version"}, Separator: "@", }, } diff --git a/internal/services/deps/lua.go b/internal/services/deps/lua.go index 39c7d11..a10a161 100644 --- a/internal/services/deps/lua.go +++ b/internal/services/deps/lua.go @@ -18,6 +18,7 @@ func NewLuaManager(language string) *LuaManager { InstallCmd: []string{"luarocks", "install"}, UninstallCmd: []string{"luarocks", "remove"}, ListCmd: []string{"luarocks", "list"}, + VerifyCmd: []string{"lua", "-v"}, Separator: " ", }, } diff --git a/internal/services/deps/manager.go b/internal/services/deps/manager.go index 139d097..6ee4b20 100644 --- a/internal/services/deps/manager.go +++ b/internal/services/deps/manager.go @@ -7,6 +7,7 @@ import ( "github.com/engigu/baihu-panel/internal/logger" "github.com/engigu/baihu-panel/internal/models" + "github.com/engigu/baihu-panel/internal/utils" ) // Manager 依赖管理器接口 @@ -16,6 +17,7 @@ type Manager interface { GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) GetInstallCommand(dep *models.Dependency) (string, error) GetReinstallAllCommand(deps []models.Dependency) (string, error) + GetVerifyCommand(langVersion string) (string, error) } // BaseManager 基础管理器,提供通用方法 @@ -24,16 +26,12 @@ type BaseManager struct { InstallCmd []string UninstallCmd []string ListCmd []string + VerifyCmd []string Separator string } func (m *BaseManager) runMiseCommand(langVersion string, cmdArgs []string) ([]byte, error) { - var args []string - if langVersion != "" { - args = []string{"mise", "exec", m.Language + "@" + langVersion, "--"} - } - args = append(args, cmdArgs...) - + args := utils.BuildMiseCommandArgsSimple(cmdArgs, m.Language, langVersion) cmd := exec.Command(args[0], args[1:]...) return cmd.CombinedOutput() } @@ -69,14 +67,10 @@ func (m *BaseManager) GetInstallCommand(dep *models.Dependency) (string, error) packageSpec = dep.Name } - var args []string - if dep.LangVersion != "" { - args = []string{"mise", "exec", m.Language + "@" + dep.LangVersion, "--"} - } - args = append(args, m.InstallCmd...) + args := append([]string{}, m.InstallCmd...) args = append(args, packageSpec) - fullCmd := strings.Join(args, " ") + fullCmd := utils.BuildMiseCommandSimple(strings.Join(args, " "), m.Language, dep.LangVersion) return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil } @@ -98,17 +92,23 @@ func (m *BaseManager) GetReinstallAllCommand(deps []models.Dependency) (string, } } - var args []string - if langVersion != "" { - args = []string{"mise", "exec", m.Language + "@" + langVersion, "--"} - } - args = append(args, m.InstallCmd...) + args := append([]string{}, m.InstallCmd...) args = append(args, packageSpecs...) - fullCmd := strings.Join(args, " ") + fullCmd := utils.BuildMiseCommandSimple(strings.Join(args, " "), m.Language, langVersion) return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil } +func (m *BaseManager) GetVerifyCommand(langVersion string) (string, error) { + var cmd string + if len(m.VerifyCmd) > 0 { + cmd = strings.Join(m.VerifyCmd, " ") + } else { + cmd = m.Language + " --version" + } + return utils.BuildMiseCommandSimple(cmd, m.Language, langVersion), nil +} + func (m *BaseManager) Uninstall(dep *models.Dependency) error { args := append([]string{}, m.UninstallCmd...) args = append(args, dep.Name) diff --git a/internal/services/deps/node.go b/internal/services/deps/node.go index fcf2bad..7a3c9cb 100644 --- a/internal/services/deps/node.go +++ b/internal/services/deps/node.go @@ -18,6 +18,7 @@ func NewNodeManager(language string) *NodeManager { InstallCmd: []string{"npm", "install", "-g"}, UninstallCmd: []string{"npm", "uninstall", "-g"}, ListCmd: []string{"npm", "list", "-g", "--depth=0", "--json"}, + VerifyCmd: []string{"node", "-v"}, Separator: "@", }, } diff --git a/internal/services/deps/perl.go b/internal/services/deps/perl.go index acb70e0..861a5a3 100644 --- a/internal/services/deps/perl.go +++ b/internal/services/deps/perl.go @@ -15,6 +15,7 @@ func NewPerlManager(language string) *PerlManager { InstallCmd: []string{"cpanm"}, // 需要系统预装 cpanm UninstallCmd: []string{"cpanm", "--uninstall"}, // 部分 cpanm 版本不支持,这是一个占位 ListCmd: []string{"perldoc", "-l"}, // 很难列出所有,暂时简单处理 + VerifyCmd: []string{"perl", "-v"}, Separator: " ", }, } diff --git a/internal/services/deps/php.go b/internal/services/deps/php.go index 3219e02..cdc62c6 100644 --- a/internal/services/deps/php.go +++ b/internal/services/deps/php.go @@ -18,6 +18,7 @@ func NewPhpManager(language string) *PhpManager { InstallCmd: []string{"composer", "global", "require"}, UninstallCmd: []string{"composer", "global", "remove"}, ListCmd: []string{"composer", "global", "show", "--name-only"}, + VerifyCmd: []string{"php", "-v"}, Separator: ":", }, } diff --git a/internal/services/deps/ruby.go b/internal/services/deps/ruby.go index dd5c899..ebfdbb2 100644 --- a/internal/services/deps/ruby.go +++ b/internal/services/deps/ruby.go @@ -18,6 +18,7 @@ func NewRubyManager(language string) *RubyManager { InstallCmd: []string{"gem", "install"}, UninstallCmd: []string{"gem", "uninstall", "-a", "-x"}, ListCmd: []string{"gem", "list", "--local"}, + VerifyCmd: []string{"ruby", "-v"}, Separator: " ", }, } diff --git a/internal/services/deps/rust.go b/internal/services/deps/rust.go index eae1c9e..5bb6a2a 100644 --- a/internal/services/deps/rust.go +++ b/internal/services/deps/rust.go @@ -18,6 +18,7 @@ func NewRustManager(language string) *RustManager { InstallCmd: []string{"cargo", "install"}, UninstallCmd: []string{"cargo", "uninstall"}, ListCmd: []string{"cargo", "install", "--list"}, + VerifyCmd: []string{"rustc", "--version"}, Separator: " v", }, } diff --git a/internal/services/mise_service.go b/internal/services/mise_service.go index 81266d6..5b8bf31 100644 --- a/internal/services/mise_service.go +++ b/internal/services/mise_service.go @@ -13,6 +13,8 @@ import ( "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/logger" "github.com/engigu/baihu-panel/internal/models" + "github.com/engigu/baihu-panel/internal/services/deps" + "github.com/engigu/baihu-panel/internal/utils" "gorm.io/gorm" ) @@ -322,3 +324,14 @@ func (s *MiseService) syncToDB(languages []MiseLanguage) { db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&models.Language{}) } } + +// GetVerifyCommand 获取环境验证命令 +func (s *MiseService) GetVerifyCommand(plugin, version string) (string, error) { + m := deps.GetManager(plugin) + if m == nil { + // 如果没找到对应的包管理器(比如 java 等不支持依赖管理的), + // 则提供一个基础的通用验证命令 + return utils.BuildMiseCommandSimple(plugin+" --version", plugin, version), nil + } + return m.GetVerifyCommand(version) +} diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index 7f20dd7..cfa55a6 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -124,7 +124,7 @@ func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest) } // 1. 创建初始日志记录 - taskLog, err := h.es.taskLogService.CreateEmptyLog(task.ID, task.Command) + taskLog, err := h.es.taskLogService.CreateEmptyLog(task.ID, req.Command) if err != nil { return nil, nil, fmt.Errorf("创建初始日志失败: %v", err) } @@ -357,7 +357,7 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor. Envs: req.Envs, Timeout: req.Timeout, Languages: task.Languages, - UseMise: true, // 本地业务任务启用 mise + UseMise: req.UseMise, // 使用请求中的 UseMise 标志 (由调度器统一处理过) }, stdout, stderr, hooks) } diff --git a/internal/utils/mise.go b/internal/utils/mise.go new file mode 100644 index 0000000..7de0318 --- /dev/null +++ b/internal/utils/mise.go @@ -0,0 +1,77 @@ +package utils + +import ( + "strings" +) + +// BuildMiseCommand 构建多语言 mise 执行命令 (字符串形式) +func BuildMiseCommand(command string, languages []map[string]string) string { + if len(languages) == 0 { + return command + } + + var builder strings.Builder + builder.WriteString("mise exec") + + for _, lang := range languages { + name := lang["name"] + version := lang["version"] + if name == "" { + continue + } + if version == "" { + version = "latest" + } + builder.WriteString(" " + name + "@" + version) + } + + builder.WriteString(" -- " + command) + return builder.String() +} + +// BuildMiseCommandArgs 构建多语言 mise 执行命令 (参数列表形式) +func BuildMiseCommandArgs(cmdArgs []string, languages []map[string]string) []string { + if len(languages) == 0 { + return cmdArgs + } + + args := []string{"mise", "exec"} + for _, lang := range languages { + name := lang["name"] + version := lang["version"] + if name == "" { + continue + } + if version == "" { + version = "latest" + } + args = append(args, name+"@"+version) + } + args = append(args, "--") + args = append(args, cmdArgs...) + return args +} + +// BuildMiseCommandSimple 构建单个语言的 mise 执行命令 +func BuildMiseCommandSimple(command string, language, version string) string { + if language == "" { + return command + } + spec := language + if version != "" { + spec += "@" + version + } + return "mise exec " + spec + " -- " + command +} + +// BuildMiseCommandArgsSimple 构建单个语言的 mise 执行命令 (参数列表形式) +func BuildMiseCommandArgsSimple(cmdArgs []string, language, version string) []string { + if language == "" { + return cmdArgs + } + spec := language + if version != "" { + spec += "@" + version + } + return append([]string{"mise", "exec", spec, "--"}, cmdArgs...) +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 46cbb47..6e3faae 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -249,7 +249,8 @@ export const api = { list: () => request('/mise/ls'), sync: () => request('/mise/sync', { method: 'POST' }), plugins: () => request('/mise/plugins'), - versions: (plugin: string) => request(`/mise/versions?plugin=${plugin}`) + versions: (plugin: string) => request(`/mise/versions?plugin=${plugin}`), + verifyCommand: (plugin: string, version: string) => request<{ command: string }>(`/mise/verify-cmd?plugin=${plugin}&version=${version}`) } } diff --git a/web/src/views/languages/Languages.vue b/web/src/views/languages/Languages.vue index 75e7ff9..a4a2f60 100644 --- a/web/src/views/languages/Languages.vue +++ b/web/src/views/languages/Languages.vue @@ -217,6 +217,15 @@ function getLangIcon(plugin: string) { return '' } +async function handleVerify(lang: MiseLanguage) { + try { + const { command } = await api.mise.verifyCommand(lang.plugin, lang.version) + runInTerminal(command) + } catch (e) { + toast.error('获取验证命令失败') + } +} + onMounted(loadLanguages) @@ -310,7 +319,7 @@ onMounted(loadLanguages) 不支持管理