feat(deps): implement dependency auto-completion and interactive installation CLI (#147)
This commit is contained in:
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"github.com/engigu/baihu-panel/cmd/builtininstall"
|
||||
"github.com/engigu/baihu-panel/cmd/depinstall"
|
||||
"github.com/engigu/baihu-panel/cmd/reposync"
|
||||
"github.com/engigu/baihu-panel/cmd/resetpwd"
|
||||
"github.com/engigu/baihu-panel/cmd/restore"
|
||||
@@ -25,5 +26,6 @@ var Handlers = map[string]CommandHandler{
|
||||
"version": version.Run,
|
||||
"-v": version.Run,
|
||||
"-V": version.Run,
|
||||
"depinstall": depinstall.Run,
|
||||
// "migrate": migrate.Run,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
package depinstall
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/engigu/baihu-panel/cmd/clibase"
|
||||
"github.com/engigu/baihu-panel/internal/database"
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
"github.com/engigu/baihu-panel/internal/services"
|
||||
"github.com/engigu/baihu-panel/internal/services/deps"
|
||||
"github.com/engigu/baihu-panel/internal/utils"
|
||||
)
|
||||
|
||||
// Run 依赖自动补全命令入口
|
||||
func Run(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Println("用法: baihu depinstall <log_id>")
|
||||
return
|
||||
}
|
||||
|
||||
logID := args[0]
|
||||
|
||||
// 初始化基础环境和数据库连接
|
||||
if err := clibase.InitContext(true); err != nil {
|
||||
fmt.Printf(">> 初始化环境失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
var log models.TaskLog
|
||||
if err := database.DB.Where("id = ?", logID).First(&log).Error; err != nil {
|
||||
fmt.Printf(">> 未找到指定的任务日志 (ID: %s): %v\n", logID, err)
|
||||
return
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
if err := database.DB.Where("id = ?", log.TaskID).First(&task).Error; err != nil {
|
||||
fmt.Printf(">> 未找到对应的任务 (TaskID: %s): %v\n", log.TaskID, err)
|
||||
return
|
||||
}
|
||||
|
||||
logOutput, err := utils.DecompressFromBase64(string(log.Output))
|
||||
if err != nil {
|
||||
fmt.Printf(">> 解压日志失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 找出任务配置的语言
|
||||
taskLangs := task.GetLanguages()
|
||||
if len(taskLangs) == 0 {
|
||||
fmt.Println(">> 提示: 当前任务未配置具体语言环境,请手动指定语言类型(例如 python3, node 等):")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
fmt.Println(">> 已取消补全。")
|
||||
return
|
||||
}
|
||||
taskLangs = append(taskLangs, map[string]string{
|
||||
"name": input,
|
||||
"version": "",
|
||||
})
|
||||
}
|
||||
|
||||
var allDetected []string
|
||||
langToPkgMap := make(map[string][]string)
|
||||
|
||||
for _, langMap := range taskLangs {
|
||||
langName := langMap["name"]
|
||||
if langName == "" {
|
||||
continue
|
||||
}
|
||||
detected, found := deps.DetectMissingDependencies(langName, logOutput)
|
||||
if found {
|
||||
langToPkgMap[langName] = detected
|
||||
allDetected = append(allDetected, detected...)
|
||||
}
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
// 如果没有检测到任何缺失的包,允许用户手动输入
|
||||
if len(allDetected) == 0 {
|
||||
fmt.Println(">> 分析完毕: 未从最近一次的任务运行日志中检测到缺失依赖模式。")
|
||||
fmt.Println(">> 您可以手动输入想要安装的依赖包名称(多个包用空格分隔,若不安装请直接回车退出):")
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
fmt.Println(">> 已退出依赖补全。")
|
||||
return
|
||||
}
|
||||
// 默认分配到任务的第一个语言环境
|
||||
defaultLang := taskLangs[0]["name"]
|
||||
langToPkgMap[defaultLang] = strings.Fields(input)
|
||||
allDetected = append(allDetected, langToPkgMap[defaultLang]...)
|
||||
} else {
|
||||
fmt.Println(">> 分析结果: 从运行日志中检测到以下缺失依赖包:")
|
||||
for langName, pkgs := range langToPkgMap {
|
||||
fmt.Printf(" [%s]: %s\n", langName, strings.Join(pkgs, ", "))
|
||||
}
|
||||
fmt.Println(">> 是否确认自动安装上述依赖包?(y/N):")
|
||||
confirm, _ := reader.ReadString('\n')
|
||||
confirm = strings.TrimSpace(strings.ToLower(confirm))
|
||||
if confirm != "y" && confirm != "yes" {
|
||||
fmt.Println(">> 用户已取消安装操作。")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("==================================================================")
|
||||
fmt.Println(">> 开始执行依赖安装,请稍候...")
|
||||
fmt.Println("==================================================================")
|
||||
|
||||
var failedPkgs []string
|
||||
depService := services.NewDependencyService()
|
||||
|
||||
for langName, pkgs := range langToPkgMap {
|
||||
var langVersion string
|
||||
for _, lm := range taskLangs {
|
||||
if lm["name"] == langName {
|
||||
langVersion = lm["version"]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m := deps.GetManager(langName)
|
||||
if m == nil {
|
||||
fmt.Printf(">> 错误: 不支持的语言类型: %s\n", langName)
|
||||
failedPkgs = append(failedPkgs, pkgs...)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
dep := &models.Dependency{
|
||||
Name: pkg,
|
||||
Language: langName,
|
||||
LangVersion: langVersion,
|
||||
}
|
||||
|
||||
cmdStr, err := m.GetInstallCommand(dep)
|
||||
if err != nil {
|
||||
fmt.Printf(">> 无法生成 %s 包 [%s] 的安装命令: %v\n", langName, pkg, err)
|
||||
failedPkgs = append(failedPkgs, pkg)
|
||||
continue
|
||||
}
|
||||
|
||||
// 去除命令末尾的 success/failed echo 重定向,因为我们需要捕获退出状态并在控制台展示原始流程
|
||||
if idx := strings.Index(cmdStr, " && echo"); idx != -1 {
|
||||
cmdStr = cmdStr[:idx]
|
||||
}
|
||||
|
||||
fmt.Printf(">> 正在安装 [%s] -> 执行指令: %s\n", pkg, cmdStr)
|
||||
|
||||
execCmd := utils.NewShellCommandCmd(cmdStr)
|
||||
execCmd.Stdout = os.Stdout
|
||||
execCmd.Stderr = os.Stderr
|
||||
execCmd.Stdin = os.Stdin
|
||||
|
||||
runErr := execCmd.Run()
|
||||
if runErr != nil {
|
||||
fmt.Printf(">> 【失败】依赖包 [%s] 安装出错。\n\n", pkg)
|
||||
failedPkgs = append(failedPkgs, pkg)
|
||||
} else {
|
||||
fmt.Printf(">> 【成功】依赖包 [%s] 安装成功!\n\n", pkg)
|
||||
// 成功后记录到依赖表
|
||||
_ = depService.Create(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("==================================================================")
|
||||
if len(failedPkgs) > 0 {
|
||||
fmt.Printf(">> 依赖补全已结束。其中以下依赖包安装失败,请用户自行判断/手动处理:\n")
|
||||
for _, fp := range failedPkgs {
|
||||
fmt.Printf(" - %s\n", fp)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(">> 恭喜!所有依赖包安装成功!")
|
||||
}
|
||||
fmt.Println("==================================================================")
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -360,7 +360,8 @@ export const api = {
|
||||
const query = new URLSearchParams({ language })
|
||||
if (lang_version) query.set('lang_version', lang_version)
|
||||
return request<Dependency[]>(`/deps/installed?${query}`)
|
||||
}
|
||||
},
|
||||
getInstallSuggestCmd: (logID: string) => request<{ command: string }>(`/deps/install-suggest-cmd?log_id=${logID}`)
|
||||
},
|
||||
agents: {
|
||||
list: () => request<Agent[]>('/agents'),
|
||||
|
||||
@@ -318,7 +318,7 @@ async function save() {
|
||||
|
||||
<template>
|
||||
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
||||
<DialogContent class="max-w-[95vw] sm:max-w-[700px] xl:max-w-[950px] p-0 overflow-hidden border-none bg-background shadow-2xl transition-all duration-300" style="text-rendering: optimizeLegibility;" @openAutoFocus.prevent>
|
||||
<DialogContent class="max-w-[95vw] sm:max-w-[700px] xl:max-w-[950px] p-0 overflow-hidden border-none bg-background shadow-2xl transition-all duration-300" style="text-rendering: optimizeLegibility;" @openAutoFocus.prevent @pointerDownOutside.prevent>
|
||||
<div class="flex flex-col max-h-[85vh]">
|
||||
<DialogHeader class="px-5 sm:px-6 pr-20 pt-6 pb-2 shrink-0">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-2">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import DirTreeSelect from '@/components/DirTreeSelect.vue'
|
||||
import { Plus, X, ChevronDown, Search, AlertCircle, Terminal, Zap, Lock, Variable } from 'lucide-vue-next'
|
||||
import { Plus, X, ChevronDown, Search, AlertCircle, Terminal, Zap, Lock, Variable, Wrench } from 'lucide-vue-next'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { api, type Task, type EnvVar, type Agent } from '@/api'
|
||||
@@ -283,7 +283,7 @@ async function save() {
|
||||
|
||||
<template>
|
||||
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
||||
<DialogContent class="max-w-[95vw] sm:max-w-[600px] xl:max-w-[850px] p-0 overflow-hidden border-none bg-background shadow-2xl transition-all duration-300" style="text-rendering: optimizeLegibility;" @openAutoFocus.prevent>
|
||||
<DialogContent class="max-w-[95vw] sm:max-w-[600px] xl:max-w-[850px] p-0 overflow-hidden border-none bg-background shadow-2xl transition-all duration-300" style="text-rendering: optimizeLegibility;" @openAutoFocus.prevent @pointerDownOutside.prevent>
|
||||
<div class="flex flex-col max-h-[85vh]">
|
||||
<DialogHeader class="px-6 pr-12 pt-6 pb-2 shrink-0 border-b border-muted/50">
|
||||
<DialogTitle class="text-xl font-bold py-2">
|
||||
@@ -354,8 +354,15 @@ async function save() {
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-start gap-3">
|
||||
<div class="sm:col-span-1" />
|
||||
<div class="sm:col-span-3">
|
||||
<div class="flex items-center gap-2.5 p-3 rounded-xl bg-amber-500/5 border border-amber-500/10 text-amber-600 dark:text-amber-400 text-[11px] leading-relaxed font-medium">
|
||||
<AlertCircle class="h-4 w-4 shrink-0 text-amber-500" /><p>请先在<b>「语言依赖」</b>中安装所需的运行时。执行脚本时将自动注入该环境。</p>
|
||||
<div class="flex flex-col gap-2 p-3 rounded-xl bg-amber-500/5 border border-amber-500/10 text-amber-600 dark:text-amber-400 text-[11px] leading-relaxed font-medium">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<AlertCircle class="h-4 w-4 shrink-0 text-amber-500" />
|
||||
<p>请先在<b>「语言依赖」</b>中安装所需的运行时。执行脚本时将自动注入该环境。</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2.5 mt-1 border-t border-amber-500/10 pt-1.5">
|
||||
<Wrench class="h-4 w-4 shrink-0 text-amber-500" />
|
||||
<p><b>依赖自动补全:</b>若执行报错提示缺失包(如 requests 等),可在任务列表的<b>「操作菜单」</b>中点击<b>「补全依赖」</b>进行一键安装。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
import { ref, onMounted, computed, watch, onUnmounted } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import BaihuDialog from '@/components/ui/BaihuDialog.vue'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import TaskDialog from './TaskDialog.vue'
|
||||
import RepoDialog from './RepoDialog.vue'
|
||||
import LogViewer from '@/views/history/LogViewer.vue'
|
||||
import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, RefreshCw, Wifi, WifiOff, Zap, ZapOff, Copy, Tag, ChevronDown, Pin, PinOff, MoreHorizontal, CalendarClock } from 'lucide-vue-next'
|
||||
import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, RefreshCw, Wifi, WifiOff, Zap, ZapOff, Copy, Tag, ChevronDown, Pin, PinOff, MoreHorizontal, CalendarClock, Wrench } from 'lucide-vue-next'
|
||||
import TagInput from '@/components/TagInput.vue'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import XTerminal from '@/components/XTerminal.vue'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -416,6 +418,31 @@ async function viewLogs(taskId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const showTerminalDialog = ref(false)
|
||||
const terminalCmd = ref('')
|
||||
|
||||
async function handleTaskDepInstall(task: Task) {
|
||||
try {
|
||||
const res = await api.logs.list({ task_id: task.id, page: 1, page_size: 1 })
|
||||
if (res.data && res.data.length > 0) {
|
||||
const latestLog = res.data[0]
|
||||
if (latestLog) {
|
||||
const cmdRes = await api.deps.getInstallSuggestCmd(latestLog.id)
|
||||
if (cmdRes && cmdRes.command) {
|
||||
terminalCmd.value = cmdRes.command
|
||||
showTerminalDialog.value = true
|
||||
}
|
||||
} else {
|
||||
toast.error('未找到该任务的运行日志,无法分析依赖')
|
||||
}
|
||||
} else {
|
||||
toast.error('该任务尚未运行,无日志可分析')
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || '获取依赖安装命令失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 视图管理
|
||||
const taskViews = ref<any[]>([])
|
||||
const newViewName = ref('')
|
||||
@@ -797,6 +824,10 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
<PinOff v-else class="h-3.5 w-3.5 mr-2 text-primary" />
|
||||
<span>{{ task.pin_type === 'top' ? '取消置顶' : '置顶任务' }}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleTaskDepInstall(task)">
|
||||
<Wrench class="h-3.5 w-3.5 mr-2 text-amber-500" />
|
||||
<span>补全依赖</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="duplicateTask(task)">
|
||||
<Copy class="h-3.5 w-3.5 mr-2" />
|
||||
<span>复制任务</span>
|
||||
@@ -881,6 +912,10 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
<PinOff v-else class="h-3.5 w-3.5 mr-2 text-primary" />
|
||||
<span>{{ task.pin_type === 'top' ? '取消置顶' : '置顶任务' }}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleTaskDepInstall(task)">
|
||||
<Wrench class="h-3.5 w-3.5 mr-2 text-amber-500" />
|
||||
<span>补全依赖</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="duplicateTask(task)">
|
||||
<Copy class="h-3.5 w-3.5 mr-2" />
|
||||
<span>复制任务</span>
|
||||
@@ -1070,5 +1105,20 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
</Button>
|
||||
</template>
|
||||
</BaihuDialog>
|
||||
|
||||
<!-- 依赖补全终端 -->
|
||||
<Dialog v-model:open="showTerminalDialog">
|
||||
<DialogContent class="w-[calc(100%-1rem)] sm:max-w-[90vw] lg:max-w-4xl h-[60vh] sm:h-[80vh] flex flex-col p-0 overflow-hidden bg-[#1e1e1e] border-none shadow-2xl [&>button]:hidden">
|
||||
<div class="flex items-center justify-between px-3 py-2 border-b border-[#3c3c3c]">
|
||||
<span class="text-xs sm:text-sm font-medium text-gray-300">依赖补全终端</span>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="showTerminalDialog = false">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<XTerminal v-if="showTerminalDialog" :initial-command="terminalCmd" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user