feat: complete mise env base code
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = <
|
||||
}
|
||||
}
|
||||
|
||||
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{})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user