package services import ( "encoding/json" "fmt" "os" "os/exec" "sort" "strings" "time" "github.com/engigu/taskpool/internal/constant" "github.com/engigu/taskpool/internal/database" "github.com/engigu/taskpool/internal/logger" "github.com/engigu/taskpool/internal/models" "github.com/engigu/taskpool/internal/services/deps" "github.com/engigu/taskpool/internal/utils" "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"` IsGlobal bool `json:"is_global"` InstallPath string `json:"install_path,omitempty"` InstalledAt string `json:"installed_at,omitempty"` // 安装日期 } type MiseSource struct { Type string `json:"type"` Path string `json:"path"` } // List 实时从系统检测 mise 环境并同步到数据库 func (s *MiseService) List() ([]MiseLanguage, error) { langs, err := s.fetchLiveLanguages() if err != nil { return nil, err } // 异步同步到数据库,确保列表响应速度 go s.syncToDB(langs) return langs, 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 = os.Environ() 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 (s *MiseService) listFallback() ([]MiseLanguage, error) { cmd := exec.Command("mise", "ls") cmd.Env = os.Environ() 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], } 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 { // 判断是否是 global path := strings.ToLower(languages[i].Source.Path) // 归一化路径分隔符 normPath := strings.ReplaceAll(path, "\\", "/") if languages[i].Source.Type == "global" { languages[i].IsGlobal = true } else if strings.Contains(normPath, "mise/config.toml") { languages[i].IsGlobal = true } 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 []string for _, lang := range languages { var model models.Language // 以 plugin 和 version 作为联合唯一标识(业务逻辑上) res := db.Where("plugin = ? AND version = ?", lang.Plugin, lang.Version).Limit(1).Find(&model) queryErr := res.Error rowsAffected := res.RowsAffected 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 queryErr == nil && rowsAffected == 0 { // 如果不存在,则创建 newLang := models.Language{ ID: utils.GenerateID(), 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{}) } } // GetVerifyCommand 获取环境验证命令 func (s *MiseService) GetVerifyCommand(plugin, version string) (string, error) { m := deps.GetManager(plugin) if m == nil { // 如果没找到对应的包管理器(比如 java 等不支持依赖管理的), // 则提供一个基础的通用验证命令 return utils.BuildMiseCommandSimple(plugin+" --version", plugin, version), nil } return m.GetVerifyCommand(version) } // UseGlobal 设置全局默认版本 func (s *MiseService) UseGlobal(plugin, version string) error { cmd := exec.Command("mise", "use", "-g", fmt.Sprintf("%s@%s", plugin, version)) cmd.Env = os.Environ() output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("mise use -g failed: %v, output: %s", err, string(output)) } return nil } // UnsetGlobal 取消全局默认版本 func (s *MiseService) UnsetGlobal(plugin, version string) error { // 使用 mise unuse -g @ 移出全局配置 target := plugin if version != "" { target = fmt.Sprintf("%s@%s", plugin, version) } cmd := exec.Command("mise", "unuse", "-g", target) cmd.Env = os.Environ() output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("mise unuse global failed: %v, output: %s", err, string(output)) } return nil } // Envs 获取全局环境变量 func (s *MiseService) Envs() (map[string]string, error) { cmd := exec.Command("mise", "set") cmd.Env = os.Environ() output, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("mise set failed: %v, output: %s", err, string(output)) } envs := make(map[string]string) lines := strings.Split(string(output), "\n") for _, line := range lines { fields := strings.Fields(line) // 期望格式: KEY VALUE SOURCE // 跳过标题行或不合规行 if len(fields) < 3 || strings.ToLower(fields[0]) == "key" { continue } // 仅显示来自全局配置文件的环境变量 if strings.Contains(strings.ToLower(fields[len(fields)-1]), "config.toml") { envs[fields[0]] = fields[1] } } return envs, nil } // SetEnv 设置全局环境变量 func (s *MiseService) SetEnv(key, value string) error { cmd := exec.Command("mise", "set", "-g", fmt.Sprintf("%s=%s", key, value)) cmd.Env = os.Environ() output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("mise set -g failed: %v, output: %s", err, string(output)) } return nil } // UnsetEnv 取消全局环境变量 func (s *MiseService) UnsetEnv(key string) error { cmd := exec.Command("mise", "unset", "-g", key) cmd.Env = os.Environ() output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("mise unset -g failed: %v, output: %s", err, string(output)) } return nil }