feat(deps): implement dependency auto-completion and interactive installation CLI (#147)
This commit is contained in:
@@ -32,6 +32,10 @@ var Commands = []CommandInfo{
|
||||
Name: "task",
|
||||
Description: "系统级任务的列表查询、触发运行、启停控制及状态查看",
|
||||
},
|
||||
{
|
||||
Name: "depinstall",
|
||||
Description: "一键补全指定任务执行日志中的缺失依赖包",
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Description: "查看当前系统版本号 (同 -v, -V)",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
@@ -397,3 +399,23 @@ func (c *DependencyController) ParseAndImport(ctx *gin.Context) {
|
||||
"command": cmd,
|
||||
})
|
||||
}
|
||||
|
||||
// GetDepInstallCommand 获取自动补全的命令,返回给前端执行
|
||||
func (c *DependencyController) GetDepInstallCommand(ctx *gin.Context) {
|
||||
logID := ctx.Query("log_id")
|
||||
if logID == "" {
|
||||
utils.BadRequest(ctx, "参数错误: log_id 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
execPath = "baihu" // 兜底
|
||||
}
|
||||
|
||||
// 构造命令,比如: "F:\workspace\baihu-panel\baihu.exe" depinstall <log_id>
|
||||
cmdStr := fmt.Sprintf("%q depinstall %s", execPath, logID)
|
||||
utils.Success(ctx, gin.H{
|
||||
"command": cmdStr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@ func registerDependencyRoutes(g *gin.RouterGroup, c *Controllers) {
|
||||
deps.POST("/batch-install-cmd", c.Dependency.GetBatchInstallCommand)
|
||||
deps.POST("/import", c.Dependency.ParseAndImport)
|
||||
deps.GET("/installed", c.Dependency.GetInstalled)
|
||||
deps.GET("/install-suggest-cmd", c.Dependency.GetDepInstallCommand)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user