Initial commit: TaskPool React panel
- React frontend with route-level code splitting - Backend rebranded from Baihu to TaskPool - DB brand migration script and local compatibility
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
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 <tool>@<version> 移出全局配置
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user