feat(deps): implement dependency auto-completion and interactive installation CLI (#147)

This commit is contained in:
duorameng
2026-07-10 08:45:26 +08:00
parent f0967abc6f
commit 3354f3d5ed
11 changed files with 442 additions and 7 deletions
+4
View File
@@ -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,
})
}
+1
View File
@@ -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)
}
}
+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])
}
}
})
}
}