feat: complete mise env base code
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package constant
|
||||
|
||||
// MainstreamMisePlugins 主流的 mise 插件列表
|
||||
var MainstreamMisePlugins = []string{
|
||||
"python", "node", "go", "rust", "ruby", "java", "php",
|
||||
"deno", "bun", "zig", "dotnet", "elixir", "erlang",
|
||||
"crystal", "lua", "julia", "nim", "perl", "scala",
|
||||
"kotlin", "clojure", "dart", "flutter", "terraform",
|
||||
}
|
||||
@@ -24,8 +24,9 @@ func NewDependencyController() *DependencyController {
|
||||
|
||||
// List 获取依赖列表
|
||||
func (c *DependencyController) List(ctx *gin.Context) {
|
||||
depType := ctx.Query("type")
|
||||
deps, err := c.service.List(depType)
|
||||
language := ctx.Query("language")
|
||||
langVersion := ctx.Query("lang_version")
|
||||
deps, err := c.service.List(language, langVersion)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "获取依赖列表失败")
|
||||
return
|
||||
@@ -40,10 +41,11 @@ func (c *DependencyController) List(ctx *gin.Context) {
|
||||
// Create 添加依赖
|
||||
func (c *DependencyController) Create(ctx *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Language string `json:"language" binding:"required"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
@@ -51,16 +53,12 @@ func (c *DependencyController) Create(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type != "py" && req.Type != "node" {
|
||||
utils.BadRequest(ctx, "类型必须是 py 或 node")
|
||||
return
|
||||
}
|
||||
|
||||
dep := &models.Dependency{
|
||||
Name: req.Name,
|
||||
Version: req.Version,
|
||||
Type: req.Type,
|
||||
Remark: req.Remark,
|
||||
Name: req.Name,
|
||||
Version: req.Version,
|
||||
Language: req.Language,
|
||||
LangVersion: req.LangVersion,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
if err := c.service.Create(dep); err != nil {
|
||||
@@ -87,13 +85,13 @@ func (c *DependencyController) Delete(ctx *gin.Context) {
|
||||
utils.SuccessMsg(ctx, "删除成功")
|
||||
}
|
||||
|
||||
// Install 安装依赖
|
||||
func (c *DependencyController) Install(ctx *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Language string `json:"language"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
@@ -101,11 +99,21 @@ func (c *DependencyController) Install(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
language := req.Language
|
||||
if language == "" {
|
||||
language = ctx.Query("language")
|
||||
}
|
||||
langVersion := req.LangVersion
|
||||
if langVersion == "" {
|
||||
langVersion = ctx.Query("lang_version")
|
||||
}
|
||||
|
||||
dep := &models.Dependency{
|
||||
Name: req.Name,
|
||||
Version: req.Version,
|
||||
Type: req.Type,
|
||||
Remark: req.Remark,
|
||||
Name: req.Name,
|
||||
Version: req.Version,
|
||||
Language: language,
|
||||
LangVersion: langVersion,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
if err := c.service.Install(dep); err != nil {
|
||||
@@ -119,6 +127,63 @@ func (c *DependencyController) Install(ctx *gin.Context) {
|
||||
utils.SuccessMsg(ctx, "安装成功")
|
||||
}
|
||||
|
||||
// GetInstallCommand 获取安装命令
|
||||
func (c *DependencyController) GetInstallCommand(ctx *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Language string `json:"language"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
}
|
||||
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
language := req.Language
|
||||
if language == "" {
|
||||
language = ctx.Query("language")
|
||||
}
|
||||
langVersion := req.LangVersion
|
||||
if langVersion == "" {
|
||||
langVersion = ctx.Query("lang_version")
|
||||
}
|
||||
|
||||
dep := &models.Dependency{
|
||||
Name: req.Name,
|
||||
Version: req.Version,
|
||||
Language: language,
|
||||
LangVersion: langVersion,
|
||||
}
|
||||
|
||||
cmd, err := c.service.GetInstallCommand(dep)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(ctx, gin.H{"command": cmd})
|
||||
}
|
||||
|
||||
// GetReinstallAllCommand 获取全部重装命令
|
||||
func (c *DependencyController) GetReinstallAllCommand(ctx *gin.Context) {
|
||||
language := ctx.Query("language")
|
||||
langVersion := ctx.Query("lang_version")
|
||||
if language == "" {
|
||||
utils.BadRequest(ctx, "缺少 language 参数")
|
||||
return
|
||||
}
|
||||
|
||||
cmd, err := c.service.GetReinstallAllCommand(language, langVersion)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(ctx, gin.H{"command": cmd})
|
||||
}
|
||||
|
||||
// Uninstall 卸载依赖
|
||||
func (c *DependencyController) Uninstall(ctx *gin.Context) {
|
||||
id, err := strconv.Atoi(ctx.Param("id"))
|
||||
@@ -128,7 +193,7 @@ func (c *DependencyController) Uninstall(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取依赖信息
|
||||
deps, _ := c.service.List("")
|
||||
deps, _ := c.service.List("", "")
|
||||
var dep *models.Dependency
|
||||
for _, d := range deps {
|
||||
if d.ID == id {
|
||||
@@ -162,7 +227,7 @@ func (c *DependencyController) Reinstall(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取依赖信息
|
||||
deps, _ := c.service.List("")
|
||||
deps, _ := c.service.List("", "")
|
||||
var dep *models.Dependency
|
||||
for _, d := range deps {
|
||||
if d.ID == id {
|
||||
@@ -186,13 +251,14 @@ func (c *DependencyController) Reinstall(ctx *gin.Context) {
|
||||
|
||||
// ReinstallAll 重新安装所有依赖
|
||||
func (c *DependencyController) ReinstallAll(ctx *gin.Context) {
|
||||
depType := ctx.Query("type")
|
||||
if depType == "" {
|
||||
utils.BadRequest(ctx, "缺少 type 参数")
|
||||
language := ctx.Query("language")
|
||||
langVersion := ctx.Query("lang_version")
|
||||
if language == "" {
|
||||
utils.BadRequest(ctx, "缺少 language 参数")
|
||||
return
|
||||
}
|
||||
|
||||
deps, err := c.service.List(depType)
|
||||
deps, err := c.service.List(language, langVersion)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "获取依赖列表失败")
|
||||
return
|
||||
@@ -215,13 +281,14 @@ func (c *DependencyController) ReinstallAll(ctx *gin.Context) {
|
||||
|
||||
// GetInstalled 获取已安装的包
|
||||
func (c *DependencyController) GetInstalled(ctx *gin.Context) {
|
||||
depType := ctx.Query("type")
|
||||
if depType == "" {
|
||||
utils.BadRequest(ctx, "缺少 type 参数")
|
||||
language := ctx.Query("language")
|
||||
langVersion := ctx.Query("lang_version")
|
||||
if language == "" {
|
||||
utils.BadRequest(ctx, "缺少 language 参数")
|
||||
return
|
||||
}
|
||||
|
||||
packages, err := c.service.GetInstalledPackages(depType)
|
||||
packages, err := c.service.GetInstalledPackages(language, langVersion)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "获取已安装包失败: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/engigu/baihu-panel/internal/services"
|
||||
"github.com/engigu/baihu-panel/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MiseController struct {
|
||||
service *services.MiseService
|
||||
}
|
||||
|
||||
func NewMiseController(service *services.MiseService) *MiseController {
|
||||
return &MiseController{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取语言列表
|
||||
func (c *MiseController) List(ctx *gin.Context) {
|
||||
langs, err := c.service.List()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "获取语言列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, langs)
|
||||
}
|
||||
|
||||
// Sync 同步本地环境到数据库
|
||||
func (c *MiseController) Sync(ctx *gin.Context) {
|
||||
if err := c.service.Sync(); err != nil {
|
||||
utils.ServerError(ctx, "同步本地环境失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Plugins 获取可用插件列表
|
||||
func (c *MiseController) Plugins(ctx *gin.Context) {
|
||||
plugins, err := c.service.Plugins()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "获取插件列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, plugins)
|
||||
}
|
||||
|
||||
// Versions 获取指定插件的可用版本列表
|
||||
func (c *MiseController) Versions(ctx *gin.Context) {
|
||||
plugin := ctx.Query("plugin")
|
||||
if plugin == "" {
|
||||
utils.BadRequest(ctx, "参数 plugin 不能为空")
|
||||
return
|
||||
}
|
||||
versions, err := c.service.Versions(plugin)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "获取版本列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, versions)
|
||||
}
|
||||
@@ -61,6 +61,8 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
||||
WorkDir string `json:"work_dir"`
|
||||
CleanConfig string `json:"clean_config"`
|
||||
Envs string `json:"envs"`
|
||||
Language string `json:"language"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
||||
workDir = resolveWorkDir(req.WorkDir)
|
||||
}
|
||||
|
||||
task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID)
|
||||
task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Language, req.LangVersion)
|
||||
|
||||
// 如果是 Agent 任务,通知 Agent;否则添加到本地 cron
|
||||
if task.AgentID != nil && *task.AgentID > 0 {
|
||||
@@ -156,6 +158,8 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
||||
CleanConfig string `json:"clean_config"`
|
||||
Envs string `json:"envs"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Language string `json:"language"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
}
|
||||
|
||||
@@ -177,7 +181,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
||||
workDir = resolveWorkDir(req.WorkDir)
|
||||
}
|
||||
|
||||
task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID)
|
||||
task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Language, req.LangVersion)
|
||||
if task == nil {
|
||||
utils.NotFound(c, "任务不存在")
|
||||
return
|
||||
|
||||
@@ -23,6 +23,7 @@ func Migrate() error {
|
||||
&models.Dependency{},
|
||||
&models.Agent{},
|
||||
&models.AgentToken{},
|
||||
&models.Language{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,5 +38,14 @@ func customMigrations() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 移除 deps 表中的 type 字段(如果存在)
|
||||
if DB.Migrator().HasColumn(&models.Dependency{}, "type") {
|
||||
if err := DB.Migrator().DropColumn(&models.Dependency{}, "type"); err != nil {
|
||||
logger.Debugf("[Database] 移除 deps.type 列失败: %v", err)
|
||||
} else {
|
||||
logger.Infof("[Database] 已成功移除 deps 表中的冗余 type 列")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ func (m *CronManager) AddTask(task CronTask) error {
|
||||
timeout := task.GetTimeout()
|
||||
workDir := task.GetWorkDir()
|
||||
envs := task.GetEnvs()
|
||||
language := task.GetLanguage()
|
||||
langVersion := task.GetLangVersion()
|
||||
useMise := task.UseMise()
|
||||
|
||||
entryID, err := m.cron.AddFunc(task.GetSchedule(), func() {
|
||||
defer func() {
|
||||
@@ -88,13 +91,16 @@ func (m *CronManager) AddTask(task CronTask) error {
|
||||
m.logger.Infof("[CronManager] 触发计划任务 #%s (%s)", taskID, name)
|
||||
|
||||
req := &ExecutionRequest{
|
||||
TaskID: taskID,
|
||||
Name: name,
|
||||
Command: cmd,
|
||||
Type: TaskTypeCron,
|
||||
Timeout: timeout,
|
||||
WorkDir: workDir,
|
||||
Envs: ParseEnvVars(envs),
|
||||
TaskID: taskID,
|
||||
Name: name,
|
||||
Command: cmd,
|
||||
Type: TaskTypeCron,
|
||||
Timeout: timeout,
|
||||
WorkDir: workDir,
|
||||
Envs: ParseEnvVars(envs),
|
||||
Language: language,
|
||||
LangVersion: langVersion,
|
||||
UseMise: useMise,
|
||||
}
|
||||
|
||||
// 如果有关联的 Scheduler,加入队列执行
|
||||
@@ -116,7 +122,12 @@ func (m *CronManager) AddTask(task CronTask) error {
|
||||
|
||||
// 初始触发一次下次运行时间通知
|
||||
go func() {
|
||||
req := &ExecutionRequest{TaskID: taskID, Name: name, Type: TaskTypeCron}
|
||||
req := &ExecutionRequest{
|
||||
TaskID: taskID,
|
||||
Name: name,
|
||||
Type: TaskTypeCron,
|
||||
UseMise: task.UseMise(),
|
||||
}
|
||||
m.triggerNextRunEvent(taskID, req)
|
||||
}()
|
||||
|
||||
|
||||
@@ -23,20 +23,27 @@ type Task interface {
|
||||
GetTimeout() int
|
||||
GetWorkDir() string
|
||||
GetEnvs() string
|
||||
GetLanguage() string
|
||||
GetLangVersion() string
|
||||
GetUseMise() bool
|
||||
}
|
||||
|
||||
// CronTask 计划任务接口
|
||||
type CronTask interface {
|
||||
Task
|
||||
GetSchedule() string
|
||||
UseMise() bool
|
||||
}
|
||||
|
||||
// Request 任务执行请求
|
||||
type Request struct {
|
||||
Command string
|
||||
WorkDir string
|
||||
Envs []string
|
||||
Timeout int // 任务超时时间(分钟)
|
||||
Command string
|
||||
WorkDir string
|
||||
Envs []string
|
||||
Timeout int // 任务超时时间(分钟)
|
||||
Language string
|
||||
LangVersion string
|
||||
UseMise bool
|
||||
}
|
||||
|
||||
// Result 任务执行结果
|
||||
@@ -96,6 +103,9 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
||||
defer cancel()
|
||||
|
||||
finalCommand := req.Command
|
||||
if req.UseMise {
|
||||
finalCommand = BuildLanguageCommand(req.Command, req.Language, req.LangVersion)
|
||||
}
|
||||
shell, args := utils.GetShellCommand(finalCommand)
|
||||
cmd := exec.CommandContext(execCtx, shell, args...)
|
||||
|
||||
@@ -272,6 +282,18 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
||||
return result, err
|
||||
}
|
||||
|
||||
// BuildLanguageCommand 构建语言环境执行命令 (使用 mise)
|
||||
func BuildLanguageCommand(command, language, version string) string {
|
||||
if language == "" {
|
||||
return command
|
||||
}
|
||||
v := version
|
||||
if v == "" {
|
||||
v = "latest"
|
||||
}
|
||||
return "mise exec " + language + "@" + v + " -- " + command
|
||||
}
|
||||
|
||||
// ParseEnvVars 解析环境变量字符串 "KEY1=VALUE1,KEY2=VALUE2"
|
||||
func ParseEnvVars(envStr string) []string {
|
||||
if envStr == "" {
|
||||
|
||||
@@ -61,15 +61,18 @@ const (
|
||||
|
||||
// ExecutionRequest 执行请求(标准接口)
|
||||
type ExecutionRequest struct {
|
||||
TaskID string // 任务 ID
|
||||
LogID uint // 日志 ID
|
||||
Name string // 任务名称
|
||||
Type TaskType // 任务类型
|
||||
Command string // 命令
|
||||
WorkDir string // 工作目录
|
||||
Envs []string // 环境变量
|
||||
Timeout int // 超时时间(分钟)
|
||||
Metadata map[string]interface{} // 额外元数据
|
||||
TaskID string // 任务 ID
|
||||
LogID uint // 日志 ID
|
||||
Name string // 任务名称
|
||||
Type TaskType // 任务类型
|
||||
Command string // 命令
|
||||
WorkDir string // 工作目录
|
||||
Envs []string // 环境变量
|
||||
Timeout int // 超时时间(分钟)
|
||||
Language string // 语言环境
|
||||
LangVersion string // 语言版本
|
||||
UseMise bool // 是否使用 mise
|
||||
Metadata map[string]interface{} // 额外元数据
|
||||
}
|
||||
|
||||
// ExecutionResult 执行结果(标准接口)
|
||||
@@ -194,10 +197,13 @@ func NewScheduler(config SchedulerConfig, handler SchedulerEventHandler) *Schedu
|
||||
executor: func(ctx context.Context, req *ExecutionRequest, stdout, stderr io.Writer) (*Result, error) {
|
||||
hooks := &schedulerHooksAdapter{handler: handler, req: req}
|
||||
return ExecuteWithHooks(ctx, 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: req.Language,
|
||||
LangVersion: req.LangVersion,
|
||||
UseMise: req.UseMise,
|
||||
}, stdout, stderr, hooks)
|
||||
},
|
||||
taskQueue: make(chan *ExecutionRequest, config.QueueSize),
|
||||
@@ -309,6 +315,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
|
||||
if req.UseMise {
|
||||
finalCommand = BuildLanguageCommand(req.Command, req.Language, req.LangVersion)
|
||||
}
|
||||
s.logger.Infof("[Scheduler] 实际执行命令: %s", finalCommand)
|
||||
|
||||
if s.config.Verbose {
|
||||
workDir := req.WorkDir
|
||||
if workDir == "" {
|
||||
|
||||
@@ -52,14 +52,16 @@ func (AgentToken) TableName() string {
|
||||
|
||||
// AgentTask Agent 任务配置(用于下发给 Agent)
|
||||
type AgentTask struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Command string `json:"command"`
|
||||
Schedule string `json:"schedule"`
|
||||
Timeout int `json:"timeout"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
Envs string `json:"envs"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Command string `json:"command"`
|
||||
Schedule string `json:"schedule"`
|
||||
Timeout int `json:"timeout"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
Envs string `json:"envs"`
|
||||
Language string `json:"language"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// AgentTaskResult Agent 上报的任务执行结果
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
)
|
||||
|
||||
// Dependency 依赖包模型
|
||||
type Dependency struct {
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Version string `json:"version" gorm:"size:50"`
|
||||
Type string `json:"type" gorm:"size:10;not null"` // py 或 node
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
Log string `json:"log" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Version string `json:"version" gorm:"size:50"`
|
||||
Language string `json:"language" gorm:"size:100;index"` // 关联语言 (node, python...)
|
||||
LangVersion string `json:"lang_version" gorm:"size:100;index"` // 关联语言版本
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
Log string `json:"log" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Dependency) TableName() string {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Language struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Plugin string `json:"plugin" gorm:"size:100;not null;index"`
|
||||
Version string `json:"version" gorm:"size:100;not null;index"`
|
||||
InstallPath string `json:"install_path" gorm:"size:255"`
|
||||
Source string `json:"source" gorm:"size:255"`
|
||||
InstalledAt *LocalTime `json:"installed_at"`
|
||||
CreatedAt LocalTime `json:"created_at"`
|
||||
UpdatedAt LocalTime `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
func (Language) TableName() string {
|
||||
return constant.TablePrefix + "languages"
|
||||
}
|
||||
@@ -44,6 +44,8 @@ type Task struct {
|
||||
WorkDir string `json:"work_dir" gorm:"size:255;default:''"` // 工作目录,为空则使用 scripts 目录
|
||||
CleanConfig string `json:"clean_config" gorm:"size:255;default:''"` // 清理配置 JSON
|
||||
Envs string `json:"envs" gorm:"size:255;default:''"` // 环境变量ID列表,逗号分隔
|
||||
Language string `json:"language" gorm:"size:50;default:''"` // 针对本地任务的语言名称 (mise)
|
||||
LangVersion string `json:"lang_version" gorm:"size:50;default:''"` // 针对本地任务的语言版本 (mise)
|
||||
AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
RunningGo string `json:"running_go" gorm:"type:text"` // 正在运行的 go routine id 数组 (JSON)
|
||||
@@ -82,6 +84,22 @@ func (t *Task) GetEnvs() string {
|
||||
return t.Envs
|
||||
}
|
||||
|
||||
func (t *Task) GetLanguage() string {
|
||||
return t.Language
|
||||
}
|
||||
|
||||
func (t *Task) GetLangVersion() string {
|
||||
return t.LangVersion
|
||||
}
|
||||
|
||||
func (t *Task) GetUseMise() bool {
|
||||
return t.AgentID == nil || *t.AgentID == 0
|
||||
}
|
||||
|
||||
func (t *Task) UseMise() bool {
|
||||
return t.GetUseMise()
|
||||
}
|
||||
|
||||
func (t *Task) GetSchedule() string {
|
||||
return t.Schedule
|
||||
}
|
||||
|
||||
@@ -8,14 +8,15 @@ import (
|
||||
|
||||
// DependencyVO 依赖包视图对象
|
||||
type DependencyVO struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Remark string `json:"remark"`
|
||||
Log string `json:"log,omitempty"` // 仅在需要时返回
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Language string `json:"language"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
Remark string `json:"remark"`
|
||||
Log string `json:"log,omitempty"` // 仅在需要时返回
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ToDependencyVO 将 Dependency 模型转换为 DependencyVO
|
||||
@@ -24,14 +25,15 @@ func ToDependencyVO(dep *models.Dependency) *DependencyVO {
|
||||
return nil
|
||||
}
|
||||
return &DependencyVO{
|
||||
ID: dep.ID,
|
||||
Name: dep.Name,
|
||||
Version: dep.Version,
|
||||
Type: dep.Type,
|
||||
Remark: dep.Remark,
|
||||
Log: dep.Log,
|
||||
CreatedAt: dep.CreatedAt,
|
||||
UpdatedAt: dep.UpdatedAt,
|
||||
ID: dep.ID,
|
||||
Name: dep.Name,
|
||||
Version: dep.Version,
|
||||
Language: dep.Language,
|
||||
LangVersion: dep.LangVersion,
|
||||
Remark: dep.Remark,
|
||||
Log: dep.Log,
|
||||
CreatedAt: dep.CreatedAt,
|
||||
UpdatedAt: dep.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ type TaskVO struct {
|
||||
WorkDir string `json:"work_dir"`
|
||||
CleanConfig string `json:"clean_config"`
|
||||
Envs string `json:"envs"`
|
||||
Language string `json:"language"`
|
||||
LangVersion string `json:"lang_version"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LastRun *models.LocalTime `json:"last_run"`
|
||||
@@ -41,6 +43,8 @@ func ToTaskVO(task *models.Task) *TaskVO {
|
||||
WorkDir: task.WorkDir,
|
||||
CleanConfig: task.CleanConfig,
|
||||
Envs: task.Envs,
|
||||
Language: task.Language,
|
||||
LangVersion: task.LangVersion,
|
||||
AgentID: task.AgentID,
|
||||
Enabled: task.Enabled,
|
||||
LastRun: task.LastRun,
|
||||
|
||||
@@ -52,6 +52,7 @@ func RegisterControllers() *Controllers {
|
||||
Settings: controllers.NewSettingsController(userService, loginLogService, executorService),
|
||||
Dependency: controllers.NewDependencyController(),
|
||||
Agent: controllers.NewAgentController(settingsService),
|
||||
Mise: controllers.NewMiseController(services.NewMiseService()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ type Controllers struct {
|
||||
Settings *controllers.SettingsController
|
||||
Dependency *controllers.DependencyController
|
||||
Agent *controllers.AgentController
|
||||
Mise *controllers.MiseController
|
||||
}
|
||||
|
||||
func mustSubFS(fsys fs.FS, dir string) fs.FS {
|
||||
@@ -202,9 +203,11 @@ func Setup(c *Controllers) *gin.Engine {
|
||||
deps.POST("", c.Dependency.Create)
|
||||
deps.DELETE("/:id", c.Dependency.Delete)
|
||||
deps.POST("/install", c.Dependency.Install)
|
||||
deps.POST("/install-cmd", c.Dependency.GetInstallCommand)
|
||||
deps.POST("/uninstall/:id", c.Dependency.Uninstall)
|
||||
deps.POST("/reinstall/:id", c.Dependency.Reinstall)
|
||||
deps.POST("/reinstall-all", c.Dependency.ReinstallAll)
|
||||
deps.POST("/reinstall-all-cmd", c.Dependency.GetReinstallAllCommand)
|
||||
deps.GET("/installed", c.Dependency.GetInstalled)
|
||||
}
|
||||
|
||||
@@ -223,6 +226,15 @@ func Setup(c *Controllers) *gin.Engine {
|
||||
agents.DELETE("/tokens/:id", c.Agent.DeleteToken)
|
||||
}
|
||||
|
||||
// Mise routes (Mise 管理)
|
||||
mise := authorized.Group("/mise")
|
||||
{
|
||||
mise.GET("/ls", c.Mise.List)
|
||||
mise.POST("/sync", c.Mise.Sync)
|
||||
mise.GET("/plugins", c.Mise.Plugins)
|
||||
mise.GET("/versions", c.Mise.Versions)
|
||||
}
|
||||
|
||||
// Agent API(供前端调用,保持在 v1 下)
|
||||
agentAPIv1 := authorized.Group("/agent")
|
||||
{
|
||||
|
||||
@@ -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