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:
2026-07-26 08:43:52 +08:00
commit e6956aa001
397 changed files with 73621 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/internal/models"
)
type BunManager struct {
BaseManager
}
func NewBunManager(language string) *BunManager {
return &BunManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"bun", "add", "-g"},
UninstallCmd: []string{"bun", "remove", "-g"},
ListCmd: []string{"bun", "pm", "ls", "-g"},
Separator: "@",
},
}
}
func (m *BunManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Tool") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/internal/models"
)
type CrystalManager struct {
BaseManager
}
func NewCrystalManager(language string) *CrystalManager {
return &CrystalManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"shards", "install"}, // 主要是针对当前目录,但 mise exec 下可以安装 tools
UninstallCmd: []string{"rm", "-rf"}, // 这里的逻辑可能不完全正确
ListCmd: []string{"shards", "list"},
Separator: " ",
},
}
}
func (m *CrystalManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Shards") {
continue
}
fields := strings.Fields(line)
if len(fields) > 1 {
packages = append(packages, models.Dependency{Name: fields[1], Language: language})
}
}
return packages, nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/internal/models"
)
type DartManager struct {
BaseManager
}
func NewDartManager(language string) *DartManager {
return &DartManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"dart", "pub", "global", "activate"},
UninstallCmd: []string{"dart", "pub", "global", "deactivate"},
ListCmd: []string{"dart", "pub", "global", "list"},
Separator: " ",
},
}
}
func (m *DartManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+26
View File
@@ -0,0 +1,26 @@
package deps
import (
"github.com/engigu/taskpool/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
}
+103
View File
@@ -0,0 +1,103 @@
package deps
import (
"regexp"
"strings"
)
// Detector 依赖检测器接口
type Detector interface {
Detect(logContent string) []string
}
// PythonDetector Python 依赖检测器
type PythonDetector struct{}
func (d *PythonDetector) Detect(logContent string) []string {
var pkgs []string
seen := make(map[string]bool)
pythonRegex1 := regexp.MustCompile(`ModuleNotFoundError: No module named '([^']+)'`)
pythonRegex2 := regexp.MustCompile(`No module named ([a-zA-Z0-9_\-]+)`)
matches := pythonRegex1.FindAllStringSubmatch(logContent, -1)
for _, m := range matches {
if len(m) > 1 {
name := strings.TrimSpace(m[1])
if name != "" && !seen[name] {
seen[name] = true
pkgs = append(pkgs, name)
}
}
}
matches2 := pythonRegex2.FindAllStringSubmatch(logContent, -1)
for _, m := range matches2 {
if len(m) > 1 {
name := strings.TrimSpace(m[1])
if name != "" && !seen[name] {
seen[name] = true
pkgs = append(pkgs, name)
}
}
}
return pkgs
}
// NodeDetector Node.js 依赖检测器
type NodeDetector struct{}
func (d *NodeDetector) Detect(logContent string) []string {
var pkgs []string
seen := make(map[string]bool)
nodeRegex1 := regexp.MustCompile(`Error: Cannot find module '([^']+)'`)
nodeRegex2 := regexp.MustCompile(`Cannot find module '([^']+)'`)
matches := nodeRegex1.FindAllStringSubmatch(logContent, -1)
for _, m := range matches {
if len(m) > 1 {
name := strings.TrimSpace(m[1])
if name != "" && !seen[name] {
seen[name] = true
pkgs = append(pkgs, name)
}
}
}
matches2 := nodeRegex2.FindAllStringSubmatch(logContent, -1)
for _, m := range matches2 {
if len(m) > 1 {
name := strings.TrimSpace(m[1])
if name != "" && !seen[name] {
seen[name] = true
pkgs = append(pkgs, name)
}
}
}
return pkgs
}
var languageDetectors = map[string]Detector{
"python": &PythonDetector{},
"python3": &PythonDetector{},
"node": &NodeDetector{},
"js": &NodeDetector{},
"ts": &NodeDetector{},
"bun": &NodeDetector{},
}
// DetectMissingDependencies 从日志内容中检测缺失的依赖包名
func DetectMissingDependencies(language, logContent string) ([]string, bool) {
lang := strings.ToLower(language)
var det Detector
for key, d := range languageDetectors {
if strings.Contains(lang, key) {
det = d
break
}
}
if det == nil {
return nil, false
}
pkgs := det.Detect(logContent)
return pkgs, len(pkgs) > 0
}
+62
View File
@@ -0,0 +1,62 @@
package deps
import (
"testing"
)
func TestDetectMissingDependencies(t *testing.T) {
tests := []struct {
name string
language string
logContent string
wantPkgs []string
wantFound bool
}{
{
name: "Python ModuleNotFoundError",
language: "python3",
logContent: "Traceback (most recent call last):\n File \"main.py\", line 1, in <module>\n import requests\nModuleNotFoundError: No module named 'requests'",
wantPkgs: []string{"requests"},
wantFound: true,
},
{
name: "Python No module named",
language: "python",
logContent: "ImportError: No module named yaml",
wantPkgs: []string{"yaml"},
wantFound: true,
},
{
name: "Node Error Cannot find module",
language: "node",
logContent: "Error: Cannot find module 'axios'\nRequire stack:\n- /app/index.js",
wantPkgs: []string{"axios"},
wantFound: true,
},
{
name: "No match",
language: "python",
logContent: "Success running script",
wantPkgs: nil,
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPkgs, gotFound := DetectMissingDependencies(tt.language, tt.logContent)
if gotFound != tt.wantFound {
t.Errorf("DetectMissingDependencies() gotFound = %v, want %v", gotFound, tt.wantFound)
}
if len(gotPkgs) != len(tt.wantPkgs) {
t.Errorf("DetectMissingDependencies() gotPkgs = %v, want %v", gotPkgs, tt.wantPkgs)
return
}
for i, p := range gotPkgs {
if p != tt.wantPkgs[i] {
t.Errorf("DetectMissingDependencies() gotPkgs[%d] = %v, want %v", i, p, tt.wantPkgs[i])
}
}
})
}
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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
}
+50
View File
@@ -0,0 +1,50 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/internal/models"
)
type ElixirManager struct {
BaseManager
}
func NewElixirManager(language string) *ElixirManager {
verifyCmd := []string{"elixir", "-v"}
if strings.Contains(strings.ToLower(language), "erlang") {
verifyCmd = []string{"erl", "+V"}
}
return &ElixirManager{
BaseManager: BaseManager{
Language: language,
InstallCmd: []string{"mix", "archive.install", "hex", "--force"},
UninstallCmd: []string{"mix", "archive.uninstall"},
ListCmd: []string{"mix", "archive"},
VerifyCmd: verifyCmd,
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
}
+46
View File
@@ -0,0 +1,46 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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", "..."},
VerifyCmd: []string{"go", "version"},
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
}
+46
View File
@@ -0,0 +1,46 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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"},
VerifyCmd: []string{"lua", "-v"},
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
}
+202
View File
@@ -0,0 +1,202 @@
package deps
import (
"errors"
"os/exec"
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/internal/models"
"github.com/engigu/taskpool/internal/utils"
)
// 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)
GetBatchInstallCommand(deps []models.Dependency) (string, error)
GetReinstallAllCommand(deps []models.Dependency) (string, error)
GetVerifyCommand(langVersion string) (string, error)
}
// BaseManager 基础管理器,提供通用方法
type BaseManager struct {
Language string
InstallCmd []string
UninstallCmd []string
ListCmd []string
VerifyCmd []string
Separator string
}
func (m *BaseManager) runMiseCommand(langVersion string, cmdArgs []string) ([]byte, error) {
args := utils.BuildMiseCommandArgsSimple(cmdArgs, m.Language, langVersion)
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 = models.BigText(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
}
args := append([]string{}, m.InstallCmd...)
args = append(args, packageSpec)
fullCmd := utils.BuildMiseCommandSimple(strings.Join(args, " "), m.Language, dep.LangVersion)
return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil
}
func (m *BaseManager) GetBatchInstallCommand(deps []models.Dependency) (string, error) {
if len(deps) == 0 {
return "echo \"没有需要安装的依赖\"", nil
}
var packageSpecs []string
var langVersion string
var language string
for _, dep := range deps {
language = dep.Language
if dep.LangVersion != "" {
langVersion = dep.LangVersion
}
if dep.Version != "" {
packageSpecs = append(packageSpecs, dep.Name+m.Separator+dep.Version)
} else {
packageSpecs = append(packageSpecs, dep.Name)
}
}
args := append([]string{}, m.InstallCmd...)
args = append(args, packageSpecs...)
fullCmd := utils.BuildMiseCommandSimple(strings.Join(args, " "), language, langVersion)
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)
}
}
args := append([]string{}, m.InstallCmd...)
args = append(args, packageSpecs...)
fullCmd := utils.BuildMiseCommandSimple(strings.Join(args, " "), m.Language, langVersion)
return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil
}
func (m *BaseManager) GetVerifyCommand(langVersion string) (string, error) {
var cmd string
if len(m.VerifyCmd) > 0 {
cmd = strings.Join(m.VerifyCmd, " ")
} else {
cmd = m.Language + " --version"
}
return utils.BuildMiseCommandSimple(cmd, m.Language, langVersion), nil
}
func (m *BaseManager) Uninstall(dep *models.Dependency) error {
args := append([]string{}, m.UninstallCmd...)
args = append(args, dep.Name)
logger.Infof("Uninstalling %s package: %s", m.Language, dep.Name)
output, err := m.runMiseCommand(dep.LangVersion, args)
if err != nil {
logger.Errorf("Uninstall failed: %v, output: %s", err, string(output))
return errors.New("卸载失败: " + string(output))
}
return nil
}
// GetManager 根据语言获取对应的管理器
func GetManager(language string) Manager {
lang := strings.ToLower(language)
if strings.Contains(lang, "python") {
return NewPythonManager(language)
}
if strings.Contains(lang, "node") {
return NewNodeManager(language)
}
if strings.Contains(lang, "ruby") {
return NewRubyManager(language)
}
if strings.Contains(lang, "go") {
return NewGoManager(language)
}
if strings.Contains(lang, "rust") {
return NewRustManager(language)
}
if strings.Contains(lang, "bun") {
return NewBunManager(language)
}
if strings.Contains(lang, "php") {
return NewPhpManager(language)
}
if strings.Contains(lang, "deno") {
return NewDenoManager(language)
}
if strings.Contains(lang, "dotnet") {
return NewDotnetManager(language)
}
if strings.Contains(lang, "elixir") || strings.Contains(lang, "erlang") {
return NewElixirManager(language)
}
if strings.Contains(lang, "lua") {
return NewLuaManager(language)
}
if strings.Contains(lang, "nim") {
return NewNimManager(language)
}
if strings.Contains(lang, "dart") || strings.Contains(lang, "flutter") {
return NewDartManager(language)
}
if strings.Contains(lang, "perl") {
return NewPerlManager(language)
}
if strings.Contains(lang, "crystal") {
return NewCrystalManager(language)
}
return nil
}
+45
View File
@@ -0,0 +1,45 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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
}
+48
View File
@@ -0,0 +1,48 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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"},
VerifyCmd: []string{"node", "-v"},
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
}
+105
View File
@@ -0,0 +1,105 @@
package deps
import (
"encoding/json"
"regexp"
"strings"
"github.com/engigu/taskpool/internal/models"
)
// ParseManifest 根据语言解析依赖清单文件内容
func ParseManifest(language, content string) ([]models.Dependency, error) {
lang := strings.ToLower(language)
if strings.Contains(lang, "python") {
return ParseRequirements(content), nil
}
if strings.Contains(lang, "node") {
return ParsePackageJson(content)
}
return []models.Dependency{}, nil
}
// ParseRequirements 解析 Python requirements.txt
func ParseRequirements(content string) []models.Dependency {
var deps []models.Dependency
// 使用正则表达式按行分割,兼容 Windows 和 Linux 的换行符
lines := regexp.MustCompile(`\r?\n`).Split(content, -1)
// 用于分割包名和版本的正则 (支持 ==, >=, <=, ~=, >, <, @)
versionRegex := regexp.MustCompile(`[=><~@]+`)
for _, line := range lines {
line = strings.TrimSpace(line)
// 忽略空行、注释行以及参数行 (以 - 开头的行如 -i, -r)
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "-") {
continue
}
// 分割名称与版本
parts := versionRegex.Split(line, 2)
name := strings.TrimSpace(parts[0])
version := ""
if len(parts) > 1 {
// 清除可能存在的后续参数,比如 requests==2.31.0 --hash=sha256:...
versionPart := strings.TrimSpace(parts[1])
// 如果有逗号分隔的多个范围限制,比如 >=1.20,<2.0,只取第一个范围作为参考版本号
if idx := strings.Index(versionPart, ","); idx != -1 {
versionPart = versionPart[:idx]
}
versionFields := strings.Fields(versionPart)
if len(versionFields) > 0 {
version = strings.TrimSpace(versionFields[0])
// 清除可能残留的首部版本符号
version = strings.TrimLeft(version, "=><~@ ")
}
}
if name != "" {
deps = append(deps, models.Dependency{
Name: name,
Version: version,
Language: "python3",
})
}
}
return deps
}
// PackageJson 代表 package.json 的结构定义
type PackageJson struct {
Dependencies map[string]string `json:"dependencies"`
DevDependencies map[string]string `json:"devDependencies"`
}
// ParsePackageJson 解析 Node.js package.json
func ParsePackageJson(content string) ([]models.Dependency, error) {
var pkg PackageJson
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
return nil, err
}
var deps []models.Dependency
collect := func(m map[string]string, isDev bool) {
for name, versionRange := range m {
// 移除 npm 常见版本范围修饰符(如 ^1.2.3 或 ~2.3.0,保留底线版本号)
version := strings.TrimLeft(versionRange, "^~>=<* ")
remark := ""
if isDev {
remark = "devDependencies"
}
deps = append(deps, models.Dependency{
Name: name,
Version: version,
Language: "node",
Remark: remark,
})
}
}
collect(pkg.Dependencies, false)
collect(pkg.DevDependencies, true)
return deps, nil
}
+87
View File
@@ -0,0 +1,87 @@
package deps
import (
"testing"
)
func TestParseRequirements(t *testing.T) {
content := `
# This is a comment
requests==2.31.0
numpy>=1.20,<2.0
gunicorn
-r other-requirements.txt
pandas ~= 1.3.0
`
deps := ParseRequirements(content)
if len(deps) != 4 {
t.Fatalf("expected 4 dependencies, got %d", len(deps))
}
expected := []struct {
name string
version string
}{
{"requests", "2.31.0"},
{"numpy", "1.20"},
{"gunicorn", ""},
{"pandas", "1.3.0"},
}
for i, exp := range expected {
if deps[i].Name != exp.name {
t.Errorf("expected name %s, got %s", exp.name, deps[i].Name)
}
if deps[i].Version != exp.version {
t.Errorf("expected version %s, got %s", exp.version, deps[i].Version)
}
if deps[i].Language != "python3" {
t.Errorf("expected language python3, got %s", deps[i].Language)
}
}
}
func TestParsePackageJson(t *testing.T) {
content := `{
"dependencies": {
"lodash": "^4.17.21",
"express": "~4.18.2"
},
"devDependencies": {
"typescript": "^5.0.4"
}
}`
deps, err := ParsePackageJson(content)
if err != nil {
t.Fatalf("failed to parse package.json: %v", err)
}
if len(deps) != 3 {
t.Fatalf("expected 3 dependencies, got %d", len(deps))
}
expected := []struct {
name string
version string
remark string
}{
{"lodash", "4.17.21", ""},
{"express", "4.18.2", ""},
{"typescript", "5.0.4", "devDependencies"},
}
for i, exp := range expected {
if deps[i].Name != exp.name {
t.Errorf("expected name %s, got %s", exp.name, deps[i].Name)
}
if deps[i].Version != exp.version {
t.Errorf("expected version %s, got %s", exp.version, deps[i].Version)
}
if deps[i].Remark != exp.remark {
t.Errorf("expected remark %s, got %s", exp.remark, deps[i].Remark)
}
if deps[i].Language != "node" {
t.Errorf("expected language node, got %s", deps[i].Language)
}
}
}
+27
View File
@@ -0,0 +1,27 @@
package deps
import (
"github.com/engigu/taskpool/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"}, // 很难列出所有,暂时简单处理
VerifyCmd: []string{"perl", "-v"},
Separator: " ",
},
}
}
func (m *PerlManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
// Perl 依赖列出比较复杂,暂时返回空
return []models.Dependency{}, nil
}
+46
View File
@@ -0,0 +1,46 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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"},
VerifyCmd: []string{"php", "-v"},
Separator: ":",
},
}
}
func (m *PhpManager) GetInstalledPackages(language, langVersion string) ([]models.Dependency, error) {
output, err := m.runMiseCommand(langVersion, m.ListCmd)
if err != nil {
logger.Warnf("GetInstalledPackages for %s failed: %v", language, err)
}
var packages []models.Dependency
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Tool") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
packages = append(packages, models.Dependency{Name: fields[0], Language: language})
}
}
return packages, nil
}
+47
View File
@@ -0,0 +1,47 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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
}
+46
View File
@@ -0,0 +1,46 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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"},
VerifyCmd: []string{"ruby", "-v"},
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
}
+46
View File
@@ -0,0 +1,46 @@
package deps
import (
"strings"
"github.com/engigu/taskpool/internal/logger"
"github.com/engigu/taskpool/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"},
VerifyCmd: []string{"rustc", "--version"},
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
}