feat: complete mise env base code

This commit is contained in:
engigu
2026-02-13 17:27:55 +08:00
parent 800ac40200
commit c1c1d69e66
49 changed files with 2724 additions and 470 deletions
+10 -8
View File
@@ -311,14 +311,16 @@ func (s *AgentService) GetTasks(agentID uint) []models.AgentTask {
envVarsStr := s.buildEnvVarsString(task.Envs)
result[i] = models.AgentTask{
ID: task.ID,
Name: task.Name,
Command: task.Command,
Schedule: task.Schedule,
Timeout: task.Timeout,
WorkDir: task.WorkDir,
Envs: envVarsStr, // 传递 "KEY1=VALUE1,KEY2=VALUE2" 格式
Enabled: task.Enabled,
ID: task.ID,
Name: task.Name,
Command: task.Command,
Schedule: task.Schedule,
Timeout: task.Timeout,
WorkDir: task.WorkDir,
Envs: envVarsStr, // 传递 "KEY1=VALUE1,KEY2=VALUE2" 格式
Language: task.Language,
LangVersion: task.LangVersion,
Enabled: task.Enabled,
}
}
+22
View File
@@ -51,6 +51,8 @@ func (s *BackupService) getTableConfigs() []tableConfig {
{"login_logs.json", s.exportTable(&[]models.LoginLog{}, false), s.restoreTable(&[]models.LoginLog{}, false)},
{"agents.json", s.exportTable(&[]models.Agent{}, true), s.restoreTable(&[]models.Agent{}, true)},
{"tokens.json", s.exportTable(&[]models.AgentToken{}, true), s.restoreTable(&[]models.AgentToken{}, true)},
{"languages.json", s.exportTable(&[]models.Language{}, true), s.restoreTable(&[]models.Language{}, true)},
{"deps.json", s.exportTable(&[]models.Dependency{}, true), s.restoreTable(&[]models.Dependency{}, true)},
}
}
@@ -153,6 +155,20 @@ func (s *BackupService) CreateBackup() (string, error) {
}
}
// 写入元数据信息
sysInfo := map[string]interface{}{
"version": "v2",
"ts": time.Now().Format("2006-01-02 15:04:05"),
}
sysFile, err := zipWriter.Create("__sys__.json")
if err != nil {
return "", err
}
sysData, _ := json.MarshalIndent(sysInfo, "", " ")
if _, err := sysFile.Write(sysData); err != nil {
return "", err
}
// 打包 scripts 文件夹
scriptsDir := constant.ScriptsWorkDir
if _, err := os.Stat(scriptsDir); err == nil {
@@ -192,6 +208,8 @@ func (s *BackupService) Restore(zipPath string) error {
tx.Unscoped().Where("1=1").Delete(&models.LoginLog{})
tx.Unscoped().Where("1=1").Delete(&models.Agent{})
tx.Unscoped().Where("1=1").Delete(&models.AgentToken{})
tx.Unscoped().Where("1=1").Delete(&models.Language{})
tx.Unscoped().Where("1=1").Delete(&models.Dependency{})
// 2. 依次恢复每个表
for _, cfg := range configs {
@@ -258,6 +276,10 @@ func (s *BackupService) restoreFromZipFile(tx *gorm.DB, f *zip.File, filename st
return &models.Agent{}
case "tokens.json":
return &models.AgentToken{}
case "languages.json":
return &models.Language{}
case "deps.json":
return &models.Dependency{}
default:
return nil
}
+45 -126
View File
@@ -2,12 +2,10 @@ package services
import (
"errors"
"os/exec"
"strings"
"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"
)
type DependencyService struct{}
@@ -17,21 +15,25 @@ func NewDependencyService() *DependencyService {
}
// List 获取依赖列表
func (s *DependencyService) List(depType string) ([]models.Dependency, error) {
var deps []models.Dependency
func (s *DependencyService) List(language, langVersion string) ([]models.Dependency, error) {
var results []models.Dependency
query := database.DB
if depType != "" {
query = query.Where("type = ?", depType)
if language != "" {
query = query.Where("language = ?", language)
}
err := query.Order("id desc").Find(&deps).Error
return deps, err
if langVersion != "" {
query = query.Where("lang_version = ?", langVersion)
}
err := query.Order("id desc").Find(&results).Error
return results, 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 {
err := database.DB.Where("name = ? AND language = ? AND lang_version = ?", dep.Name, dep.Language, dep.LangVersion).First(&existing).Error
if err == nil {
return errors.New("依赖已存在")
}
return database.DB.Create(dep).Error
@@ -44,134 +46,51 @@ func (s *DependencyService) Delete(id int) 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
m := deps.GetManager(dep.Language)
if m == nil {
return errors.New("不支持的依赖类型: " + dep.Language)
}
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
return m.Install(dep)
}
// 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("不支持的依赖类型")
m := deps.GetManager(dep.Language)
if m == nil {
return errors.New("不支持的依赖类型: " + dep.Language)
}
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
return m.Uninstall(dep)
}
// 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("不支持的依赖类型")
func (s *DependencyService) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
m := deps.GetManager(language)
if m == nil {
return nil, errors.New("不支持的依赖类型: " + language)
}
return m.GetInstalledPackages(language, langVersion)
}
// getPipPackages 获取 pip 已安装的包
func (s *DependencyService) getPipPackages() ([]models.Dependency, error) {
cmd := exec.Command("pip", "list", "--format=freeze")
output, err := cmd.Output()
// GetInstallCommand 获取安装命令
func (s *DependencyService) GetInstallCommand(dep *models.Dependency) (string, error) {
m := deps.GetManager(dep.Language)
if m == nil {
return "", errors.New("不支持的依赖类型: " + dep.Language)
}
return m.GetInstallCommand(dep)
}
// GetReinstallAllCommand 获取全部重装命令
func (s *DependencyService) GetReinstallAllCommand(language, langVersion string) (string, error) {
m := deps.GetManager(language)
if m == nil {
return "", errors.New("不支持的依赖类型: " + language)
}
deps_list, err := s.List(language, langVersion)
if err != nil {
return nil, err
return "", 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
return m.GetReinstallAllCommand(deps_list)
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type BunManager struct {
BaseManager
}
func NewBunManager(language string) *BunManager {
return &BunManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"bun", "add", "-g"},
UninstallCmd: []string{"bun", "remove", "-g"},
ListCmd: []string{"bun", "pm", "ls", "-g"},
Separator: "@",
},
}
}
func (m *BunManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Tool") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type CrystalManager struct {
BaseManager
}
func NewCrystalManager(language string) *CrystalManager {
return &CrystalManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"shards", "install"}, // 主要是针对当前目录,但 mise exec 下可以安装 tools
UninstallCmd: []string{"rm", "-rf"}, // 这里的逻辑可能不完全正确
ListCmd: []string{"shards", "list"},
Separator: " ",
},
}
}
func (m *CrystalManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Shards") {
continue
}
fields := strings.Fields(line)
if len(fields) > 1 {
packages = append(packages, models.Dependency{Name: fields[1], Language: language})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type DartManager struct {
BaseManager
}
func NewDartManager(language string) *DartManager {
return &DartManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"dart", "pub", "global", "activate"},
UninstallCmd: []string{"dart", "pub", "global", "deactivate"},
ListCmd: []string{"dart", "pub", "global", "list"},
Separator: " ",
},
}
}
func (m *DartManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+26
View File
@@ -0,0 +1,26 @@
package deps
import (
"github.com/engigu/baihu-panel/internal/models"
)
type DenoManager struct {
BaseManager
}
func NewDenoManager(language string) *DenoManager {
return &DenoManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"deno", "install", "--global", "-A"},
UninstallCmd: []string{"deno", "uninstall"},
ListCmd: []string{"ls", "-1", "/root/.deno/bin"}, // 这是一个简单的 fallback 尝试
Separator: "@",
},
}
}
func (m *DenoManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
// Deno 没有官方的 list 命令,暂时留空或通过文件系统猜测
return []models.Dependency{}, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type DotnetManager struct {
BaseManager
}
func NewDotnetManager(language string) *DotnetManager {
return &DotnetManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"dotnet", "tool", "install", "-g"},
UninstallCmd: []string{"dotnet", "tool", "uninstall", "-g"},
ListCmd: []string{"dotnet", "tool", "list", "-g"},
Separator: " ",
},
}
}
func (m *DotnetManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for i, line := range lines {
line = strings.TrimSpace(line)
if i < 2 || line == "" { // 跳过表头
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language, Version: fields[1]})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type ElixirManager struct {
BaseManager
}
func NewElixirManager(language string) *ElixirManager {
return &ElixirManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"mix", "archive.install", "hex", "--force"},
UninstallCmd: []string{"mix", "archive.uninstall"},
ListCmd: []string{"mix", "archive"},
Separator: " ",
},
}
}
func (m *ElixirManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "*") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type GoManager struct {
BaseManager
}
func NewGoManager(language string) *GoManager {
return &GoManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"go", "install"},
UninstallCmd: []string{"go", "clean", "-i"},
ListCmd: []string{"go", "list", "..."},
Separator: "@",
},
}
}
func (m *GoManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Tool") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type LuaManager struct {
BaseManager
}
func NewLuaManager(language string) *LuaManager {
return &LuaManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"luarocks", "install"},
UninstallCmd: []string{"luarocks", "remove"},
ListCmd: []string{"luarocks", "list"},
Separator: " ",
},
}
}
func (m *LuaManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Rock") || strings.HasPrefix(line, "--") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+174
View File
@@ -0,0 +1,174 @@
package deps
import (
"errors"
"os/exec"
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
// Manager 依赖管理器接口
type Manager interface {
Install(dep *models.Dependency) error
Uninstall(dep *models.Dependency) error
GetInstalledPackages(language, langVersion string) ([]models.Dependency, error)
GetInstallCommand(dep *models.Dependency) (string, error)
GetReinstallAllCommand(deps []models.Dependency) (string, error)
}
// BaseManager 基础管理器,提供通用方法
type BaseManager struct {
Language string
InstallCmd []string
UninstallCmd []string
ListCmd []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...)
cmd := exec.Command(args[0], args[1:]...)
return cmd.CombinedOutput()
}
func (m *BaseManager) Install(dep *models.Dependency) error {
var packageSpec string
if dep.Version != "" {
packageSpec = dep.Name + m.Separator + dep.Version
} else {
packageSpec = dep.Name
}
args := append([]string{}, m.InstallCmd...)
args = append(args, packageSpec)
logger.Infof("Installing %s package: %s", m.Language, packageSpec)
output, err := m.runMiseCommand(dep.LangVersion, args)
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
}
func (m *BaseManager) GetInstallCommand(dep *models.Dependency) (string, error) {
var packageSpec string
if dep.Version != "" {
packageSpec = dep.Name + m.Separator + dep.Version
} else {
packageSpec = dep.Name
}
var args []string
if dep.LangVersion != "" {
args = []string{"mise", "exec", m.Language + "@" + dep.LangVersion, "--"}
}
args = append(args, m.InstallCmd...)
args = append(args, packageSpec)
fullCmd := strings.Join(args, " ")
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
}
var packageSpecs []string
var langVersion string
for _, dep := range deps {
if dep.LangVersion != "" {
langVersion = dep.LangVersion
}
if dep.Version != "" {
packageSpecs = append(packageSpecs, dep.Name+m.Separator+dep.Version)
} else {
packageSpecs = append(packageSpecs, dep.Name)
}
}
var args []string
if langVersion != "" {
args = []string{"mise", "exec", m.Language + "@" + langVersion, "--"}
}
args = append(args, m.InstallCmd...)
args = append(args, packageSpecs...)
fullCmd := strings.Join(args, " ")
return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil
}
func (m *BaseManager) Uninstall(dep *models.Dependency) error {
args := append([]string{}, m.UninstallCmd...)
args = append(args, dep.Name)
logger.Infof("Uninstalling %s package: %s", m.Language, dep.Name)
output, err := m.runMiseCommand(dep.LangVersion, args)
if err != nil {
logger.Errorf("Uninstall failed: %v, output: %s", err, string(output))
return errors.New("卸载失败: " + string(output))
}
return nil
}
// GetManager 根据语言获取对应的管理器
func GetManager(language string) Manager {
lang := strings.ToLower(language)
if strings.Contains(lang, "python") {
return NewPythonManager(language)
}
if strings.Contains(lang, "node") {
return NewNodeManager(language)
}
if strings.Contains(lang, "ruby") {
return NewRubyManager(language)
}
if strings.Contains(lang, "go") {
return NewGoManager(language)
}
if strings.Contains(lang, "rust") {
return NewRustManager(language)
}
if strings.Contains(lang, "bun") {
return NewBunManager(language)
}
if strings.Contains(lang, "php") {
return NewPhpManager(language)
}
if strings.Contains(lang, "deno") {
return NewDenoManager(language)
}
if strings.Contains(lang, "dotnet") {
return NewDotnetManager(language)
}
if strings.Contains(lang, "elixir") || strings.Contains(lang, "erlang") {
return NewElixirManager(language)
}
if strings.Contains(lang, "lua") {
return NewLuaManager(language)
}
if strings.Contains(lang, "nim") {
return NewNimManager(language)
}
if strings.Contains(lang, "dart") || strings.Contains(lang, "flutter") {
return NewDartManager(language)
}
if strings.Contains(lang, "perl") {
return NewPerlManager(language)
}
if strings.Contains(lang, "crystal") {
return NewCrystalManager(language)
}
return nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type NimManager struct {
BaseManager
}
func NewNimManager(language string) *NimManager {
return &NimManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"nimble", "install", "-y"},
UninstallCmd: []string{"nimble", "uninstall", "-y"},
ListCmd: []string{"nimble", "list", "-i"},
Separator: "@",
},
}
}
func (m *NimManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "[") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+47
View File
@@ -0,0 +1,47 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type NodeManager struct {
BaseManager
}
func NewNodeManager(language string) *NodeManager {
return &NodeManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"npm", "install", "-g"},
UninstallCmd: []string{"npm", "uninstall", "-g"},
ListCmd: []string{"npm", "list", "-g", "--depth=0", "--json"},
Separator: "@",
},
}
}
func (m *NodeManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(line, `"version"`) || strings.HasPrefix(line, "{") || strings.HasPrefix(line, "}") || strings.HasPrefix(line, "]") {
continue
}
if strings.HasPrefix(line, `"`) && strings.Contains(line, ":") {
name := strings.Trim(strings.Split(line, ":")[0], `" `)
if name != "" && name != "dependencies" && name != "name" {
packages = append(packages, models.Dependency{Name: name, Language: language})
}
}
}
return packages, nil
}
+26
View File
@@ -0,0 +1,26 @@
package deps
import (
"github.com/engigu/baihu-panel/internal/models"
)
type PerlManager struct {
BaseManager
}
func NewPerlManager(language string) *PerlManager {
return &PerlManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"cpanm"}, // 需要系统预装 cpanm
UninstallCmd: []string{"cpanm", "--uninstall"}, // 部分 cpanm 版本不支持,这是一个占位
ListCmd: []string{"perldoc", "-l"}, // 很难列出所有,暂时简单处理
Separator: " ",
},
}
}
func (m *PerlManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
// Perl 依赖列出比较复杂,暂时返回空
return []models.Dependency{}, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type PhpManager struct {
BaseManager
}
func NewPhpManager(language string) *PhpManager {
return &PhpManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"composer", "global", "require"},
UninstallCmd: []string{"composer", "global", "remove"},
ListCmd: []string{"composer", "global", "show", "--name-only"},
Separator: ":",
},
}
}
func (m *PhpManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Tool") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+47
View File
@@ -0,0 +1,47 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type PythonManager struct {
BaseManager
}
func NewPythonManager(language string) *PythonManager {
return &PythonManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"pip", "install"},
UninstallCmd: []string{"pip", "uninstall", "-y"},
ListCmd: []string{"pip", "list", "--format=freeze"},
Separator: "==",
},
}
}
func (m *PythonManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, 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], Language: language}
if len(parts) > 1 {
pkg.Version = parts[1]
}
packages = append(packages, pkg)
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type RubyManager struct {
BaseManager
}
func NewRubyManager(language string) *RubyManager {
return &RubyManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"gem", "install"},
UninstallCmd: []string{"gem", "uninstall", "-a", "-x"},
ListCmd: []string{"gem", "list", "--local"},
Separator: " ",
},
}
}
func (m *RubyManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Tool") || strings.HasPrefix(line, "(") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
)
type RustManager struct {
BaseManager
}
func NewRustManager(language string) *RustManager {
return &RustManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"cargo", "install"},
UninstallCmd: []string{"cargo", "uninstall"},
ListCmd: []string{"cargo", "install", "--list"},
Separator: " v",
},
}
}
func (m *RustManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, " v") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+322
View File
@@ -0,0 +1,322 @@
package services
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"sort"
"strings"
"time"
"github.com/engigu/baihu-panel/internal/constant"
"github.com/engigu/baihu-panel/internal/database"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
"gorm.io/gorm"
)
type MiseService struct{}
func NewMiseService() *MiseService {
return &MiseService{}
}
type MiseLanguage struct {
Plugin string `json:"plugin"`
Version string `json:"version"`
Source MiseSource `json:"source"`
Active bool `json:"active"`
InstallPath string `json:"install_path,omitempty"`
InstalledAt string `json:"installed_at,omitempty"` // 安装日期
}
type MiseSource struct {
Type string `json:"type"`
Path string `json:"path"`
}
// List 从数据库获取已保存的语言列表
func (s *MiseService) List() ([]MiseLanguage, error) {
db := database.GetDB()
var models []models.Language
if err := db.Order("installed_at DESC, plugin ASC").Find(&models).Error; err != nil {
return nil, err
}
result := make([]MiseLanguage, 0, len(models))
for _, m := range models {
lang := MiseLanguage{
Plugin: m.Plugin,
Version: m.Version,
InstallPath: m.InstallPath,
Active: true,
}
lang.Source.Path = m.Source
if m.InstalledAt != nil {
lang.InstalledAt = time.Time(*m.InstalledAt).Format("2006-01-02 15:04:05")
}
result = append(result, lang)
}
return result, nil
}
// Sync 实时检测本地 mise 环境并同步到数据库
func (s *MiseService) Sync() error {
// 获取实时数据
langs, err := s.fetchLiveLanguages()
if err != nil {
return err
}
// 同步到数据库
s.syncToDB(langs)
return nil
}
// fetchLiveLanguages 实时从系统检测 mise 语言列表
func (s *MiseService) fetchLiveLanguages() ([]MiseLanguage, error) {
// 使用 --json 获取格式化数据
cmd := exec.Command("mise", "ls", "--json")
// 强制禁用颜色和交互
cmd.Env = append(cmd.Env, "MISE_NO_COLOR=1", "TERM=dumb")
output, err := cmd.Output()
if err != nil {
logger.Warnf("[Mise] mise ls --json failed: %v", err)
return s.listFallback()
}
// 1. 尝试解析为数组格式 [{}, {}]
var languages []MiseLanguage
if err := json.Unmarshal(output, &languages); err == nil {
s.enrichInstallDates(languages)
s.enrichSourceInfo(languages)
s.sortByInstallDate(languages)
return languages, nil
}
// 2. 如果失败,尝试解析为对象格式 {"node": [{}], "python": [{}]}
var langMap map[string][]MiseLanguage
if err := json.Unmarshal(output, &langMap); err == nil {
var result []MiseLanguage
for plugin, items := range langMap {
for _, item := range items {
// 填充插件名称(对象格式中插件名通常是 key)
if item.Plugin == "" {
item.Plugin = plugin
}
result = append(result, item)
}
}
s.enrichInstallDates(result)
s.enrichSourceInfo(result)
s.sortByInstallDate(result)
return result, nil
}
return s.listFallback()
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func (s *MiseService) listFallback() ([]MiseLanguage, error) {
cmd := exec.Command("mise", "ls")
cmd.Env = append(cmd.Env, "MISE_NO_COLOR=1", "TERM=dumb")
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("mise ls failed: %v, output: %s", err, string(output))
}
lines := strings.Split(string(output), "\n")
languages := []MiseLanguage{}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(strings.ToLower(line), "tool") {
continue
}
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
lang := MiseLanguage{
Plugin: parts[0],
Version: parts[1],
Active: true,
}
if len(parts) >= 3 {
lang.Source = MiseSource{Path: parts[2]}
}
languages = append(languages, lang)
}
return languages, nil
}
// Plugins 获取主流的 mise 插件列表 (固定返回以确保速度和稳定性)
func (s *MiseService) Plugins() ([]string, error) {
mainstream := constant.MainstreamMisePlugins
logger.Infof("[Mise] Returning fixed list of %d mainstream plugins", len(mainstream))
return mainstream, nil
}
// Versions 获取指定插件的所有可用版本
func (s *MiseService) Versions(plugin string) ([]string, error) {
if plugin == "" {
return []string{}, nil
}
// 只获取最新版本列表
cmd := exec.Command("mise", "ls-remote", plugin)
cmd.Env = append(cmd.Env, "MISE_NO_COLOR=1", "TERM=dumb")
output, err := cmd.CombinedOutput()
if err != nil && len(output) == 0 {
logger.Errorf("[Mise] Fetch versions for %s failed: %v", plugin, err)
return []string{}, nil
}
lines := strings.Split(string(output), "\n")
var versions []string
// 倒序排列,优先显示新版本
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" || strings.Contains(line, " ") || strings.Contains(line, "Usage:") || line == "latest" {
continue
}
versions = append(versions, line)
if len(versions) >= 300 {
break
}
}
return versions, nil
}
// enrichInstallDates 为语言列表添加安装日期信息
func (s *MiseService) enrichInstallDates(languages []MiseLanguage) {
for i := range languages {
if languages[i].InstallPath != "" {
if installDate := s.getInstallDate(languages[i].InstallPath); installDate != "" {
languages[i].InstalledAt = installDate
}
}
}
}
// enrichSourceInfo 为语言列表添加来源信息
func (s *MiseService) enrichSourceInfo(languages []MiseLanguage) {
for i := range languages {
// 如果source为空,使用install_path作为来源
if languages[i].Source.Type == "" && languages[i].Source.Path == "" {
if languages[i].InstallPath != "" {
languages[i].Source.Path = languages[i].InstallPath
}
}
}
}
// getInstallDate 获取安装路径的创建时间
func (s *MiseService) getInstallDate(installPath string) string {
if installPath == "" {
return ""
}
fileInfo, err := os.Stat(installPath)
if err != nil {
logger.Debugf("[Mise] Failed to stat install path %s: %v", installPath, err)
return ""
}
// 获取修改时间作为安装时间的近似值
modTime := fileInfo.ModTime()
return modTime.Format("2006-01-02 15:04:05")
}
// sortByInstallDate 按安装时间降序排序(最新的在前面)
func (s *MiseService) sortByInstallDate(languages []MiseLanguage) {
sort.Slice(languages, func(i, j int) bool {
// 如果都有安装时间,按时间降序排序
if languages[i].InstalledAt != "" && languages[j].InstalledAt != "" {
return languages[i].InstalledAt > languages[j].InstalledAt
}
// 有安装时间的排在前面
if languages[i].InstalledAt != "" {
return true
}
if languages[j].InstalledAt != "" {
return false
}
// 都没有安装时间,按插件名排序
return languages[i].Plugin < languages[j].Plugin
})
}
// syncToDB 将实时检测到的语言信息同步到数据库表中
func (s *MiseService) syncToDB(languages []MiseLanguage) {
db := database.GetDB()
if db == nil {
return
}
var currentIds []uint
for _, lang := range languages {
var model models.Language
// 以 plugin 和 version 作为联合唯一标识(业务逻辑上)
err := db.Where("plugin = ? AND version = ?", lang.Plugin, lang.Version).First(&model).Error
sourceStr := ""
if lang.Source.Path != "" {
sourceStr = lang.Source.Path
} else if lang.Source.Type != "" {
sourceStr = lang.Source.Type
}
var installTime *models.LocalTime
if lang.InstalledAt != "" {
t, err := time.Parse("2006-01-02 15:04:05", lang.InstalledAt)
if err == nil {
lt := models.LocalTime(t)
installTime = &lt
}
}
if err != nil {
// 如果不存在,则创建
newLang := models.Language{
Plugin: lang.Plugin,
Version: lang.Version,
InstallPath: lang.InstallPath,
Source: sourceStr,
InstalledAt: installTime,
}
if err := db.Create(&newLang).Error; err == nil {
currentIds = append(currentIds, newLang.ID)
}
} else {
// 如果已存在,更新可能变动的信息
updates := map[string]interface{}{
"install_path": lang.InstallPath,
"source": sourceStr,
"installed_at": installTime,
}
db.Model(&model).Updates(updates)
currentIds = append(currentIds, model.ID)
}
}
// 清理数据库中存在但实际 mise 已经卸载的记录
if len(currentIds) > 0 {
db.Where("id NOT IN ?", currentIds).Delete(&models.Language{})
} else {
// 如果本地一个都没有,清空表
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&models.Language{})
}
}
+8 -4
View File
@@ -326,6 +326,7 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.
WorkDir: req.WorkDir,
Envs: req.Envs,
Timeout: req.Timeout,
UseMise: false, // 系统任务不使用 mise
}, stdout, stderr)
}
@@ -351,10 +352,13 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.
// 本地任务
hooks := &LocalTaskHooks{es: es, logID: req.LogID}
return executor.ExecuteWithHooks(ctx, executor.Request{
Command: req.Command,
WorkDir: req.WorkDir,
Envs: req.Envs,
Timeout: req.Timeout,
Command: req.Command,
WorkDir: req.WorkDir,
Envs: req.Envs,
Timeout: req.Timeout,
Language: task.Language,
LangVersion: task.LangVersion,
UseMise: true, // 本地业务任务启用 mise
}, stdout, stderr, hooks)
}
+6 -2
View File
@@ -11,7 +11,7 @@ func NewTaskService() *TaskService {
return &TaskService{}
}
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint) *models.Task {
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, language, langVersion string) *models.Task {
if taskType == "" {
taskType = "task"
}
@@ -25,6 +25,8 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w
WorkDir: workDir,
CleanConfig: cleanConfig,
Envs: envs,
Language: language,
LangVersion: langVersion,
AgentID: agentID,
Enabled: true,
}
@@ -65,7 +67,7 @@ func (ts *TaskService) GetTaskByID(id int) *models.Task {
return &task
}
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint) *models.Task {
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, language, langVersion string) *models.Task {
var task models.Task
if err := database.DB.First(&task, id).Error; err != nil {
return nil
@@ -79,6 +81,8 @@ func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeou
task.Envs = envs
task.Enabled = enabled
task.AgentID = agentID
task.Language = language
task.LangVersion = langVersion
if taskType != "" {
task.Type = taskType
}