From f498c4fcd5566f23cf7d8ae376d4093905cb981e Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Wed, 13 May 2026 17:17:50 +0800 Subject: [PATCH] feat: add cmd task call --- cmd/builtininstall/builtininstall.go | 21 +- cmd/cmd.go | 2 + cmd/migrate/migrate.go | 2 +- cmd/reposync/reposync.go | 53 ++-- cmd/resetpwd/resetpwd.go | 23 +- cmd/restore/restore.go | 23 +- cmd/task/task.go | 399 ++++++++++++++++++++++++ internal/bootstrap/bootstrap.go | 11 +- internal/bootstrap/client.go | 58 ++++ internal/constant/commands.go | 4 + internal/controllers/task_controller.go | 77 +++++ internal/router/api_routes.go | 2 + main.go | 2 +- 13 files changed, 619 insertions(+), 58 deletions(-) create mode 100644 cmd/task/task.go create mode 100644 internal/bootstrap/client.go diff --git a/cmd/builtininstall/builtininstall.go b/cmd/builtininstall/builtininstall.go index f20011e..b34df8e 100644 --- a/cmd/builtininstall/builtininstall.go +++ b/cmd/builtininstall/builtininstall.go @@ -12,17 +12,24 @@ import ( "github.com/engigu/baihu-panel/internal/utils" ) +func printHelp() { + fmt.Fprintf(os.Stderr, "\n白虎面板内建依赖安装工具\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu builtininstall\n\n") + fmt.Fprintf(os.Stderr, "说明:\n") + fmt.Fprintf(os.Stderr, " 此命令将为系统中已安装的 Node.js 和 Python 环境安装白虎面板所需的内建依赖包。\n\n") +} + // Run 执行内建包安装逻辑 func Run(args []string) { - fs := flag.NewFlagSet("builtininstall", flag.ExitOnError) - fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板内建依赖安装工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu builtininstall\n\n") - fmt.Fprintf(os.Stderr, "说明:\n") - fmt.Fprintf(os.Stderr, " 此命令将为系统中已安装的 Node.js 和 Python 环境安装白虎面板所需的内建依赖包。\n\n") + if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") { + printHelp() + return } + fs := flag.NewFlagSet("builtininstall", flag.ExitOnError) + fs.Usage = printHelp + if err := fs.Parse(args); err != nil { return } diff --git a/cmd/cmd.go b/cmd/cmd.go index f3e96a1..5f9cf92 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -5,6 +5,7 @@ import ( "github.com/engigu/baihu-panel/cmd/reposync" "github.com/engigu/baihu-panel/cmd/resetpwd" "github.com/engigu/baihu-panel/cmd/restore" + "github.com/engigu/baihu-panel/cmd/task" // "github.com/engigu/baihu-panel/cmd/migrate" ) @@ -17,5 +18,6 @@ var Handlers = map[string]CommandHandler{ "resetpwd": resetpwd.Run, "restore": restore.Run, "builtininstall": builtininstall.Run, + "task": task.Run, // "migrate": migrate.Run, } diff --git a/cmd/migrate/migrate.go b/cmd/migrate/migrate.go index 6321e64..7baf8f5 100644 --- a/cmd/migrate/migrate.go +++ b/cmd/migrate/migrate.go @@ -11,7 +11,7 @@ func Run(args []string) { // 初始化基础环境(配置和数据库,但不运行常规 Migrate,因为我们想手动控) // 不过 bootstrap.New() 会调用 Migrate(). // 我们可以调用 InitBasic() - app := bootstrap.InitBasic() + app := bootstrap.InitBasicForCmd() if app == nil { fmt.Println("Failed to initialize app") return diff --git a/cmd/reposync/reposync.go b/cmd/reposync/reposync.go index 1d0834a..0295fdd 100644 --- a/cmd/reposync/reposync.go +++ b/cmd/reposync/reposync.go @@ -2,7 +2,6 @@ package reposync import ( "bytes" - "encoding/json" "flag" "fmt" "io" @@ -14,8 +13,8 @@ import ( "strings" "time" + "github.com/engigu/baihu-panel/internal/bootstrap" "github.com/engigu/baihu-panel/internal/constant" - "github.com/engigu/baihu-panel/internal/services" "github.com/engigu/baihu-panel/internal/services/repo" "github.com/engigu/baihu-panel/internal/utils" ) @@ -69,7 +68,7 @@ func Run(args []string) { fs.StringVar(&cfg.PreCommand, "pre-command", "", "Default pre-command for discovered tasks") fs.StringVar(&cfg.PostCommand, "post-command", "", "Default post-command for discovered tasks") - fs.Usage = func() { + printHelp := func() { fmt.Fprintf(os.Stderr, "\n白虎面板仓库同步工具 (Reposync)\n\n") fmt.Fprintf(os.Stderr, "用法:\n") fmt.Fprintf(os.Stderr, " baihu reposync [参数]\n\n") @@ -79,6 +78,13 @@ func Run(args []string) { fmt.Fprintf(os.Stderr, " baihu reposync --source-url https://github.com/xxx/repo.git --target-path $SCRIPTS_DIR$/repo1\n\n") } + if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") { + printHelp() + return + } + + fs.Usage = printHelp + if err := fs.Parse(args); err != nil { return } @@ -164,34 +170,19 @@ func getActualRepoDir(cfg Config) string { } func notifyMainServerToSyncRepoTasks(repoID string, upsertedIDs []string, deletedIDs []string) { - appCfg := services.GetConfig() - if appCfg != nil { - url := fmt.Sprintf("http://127.0.0.1:%d/internal/tasks/sync-repo-status", appCfg.Server.Port) - payload := map[string]interface{}{ - "repo_id": repoID, - "upserted_ids": upsertedIDs, - "deleted_ids": deletedIDs, - } - jsonData, _ := json.Marshal(payload) - settings := services.NewSettingsService() - secret := settings.Get("security", "secret") // constant.SectionSecurity = "security", constant.KeySecret = "secret" - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err == nil { - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Internal-Token", secret) - resp, reqErr := http.DefaultClient.Do(req) - if reqErr == nil { - defer resp.Body.Close() - if resp.StatusCode == 200 { - fmt.Println(">> [通知] 已成功将变动任务增量同步至主程序调度器") - } else { - fmt.Printf(">> [通知] 调度器刷新异常,主程序响应状态码: %d\n", resp.StatusCode) - } - } else { - fmt.Printf(">> [通知] 无法连接到主程序进行增量刷新: %v\n", reqErr) - } - } + _, statusCode, err := bootstrap.SendInternalRequest("POST", "/internal/tasks/sync-repo-status", map[string]interface{}{ + "repo_id": repoID, + "upserted_ids": upsertedIDs, + "deleted_ids": deletedIDs, + }) + if err != nil { + fmt.Printf(">> [通知] 无法连接到主程序进行增量刷新: %v\n", err) + return + } + if statusCode == 200 { + fmt.Println(">> [通知] 已成功将变动任务增量同步至主程序调度器") + } else { + fmt.Printf(">> [通知] 调度器刷新异常,主程序响应状态码: %d\n", statusCode) } } diff --git a/cmd/resetpwd/resetpwd.go b/cmd/resetpwd/resetpwd.go index 531dc54..e906bb0 100644 --- a/cmd/resetpwd/resetpwd.go +++ b/cmd/resetpwd/resetpwd.go @@ -12,22 +12,29 @@ import ( "github.com/engigu/baihu-panel/internal/utils" ) +func printHelp() { + fmt.Fprintf(os.Stderr, "\n白虎面板用户密码重置工具\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu resetpwd [用户名]\n\n") + fmt.Fprintf(os.Stderr, "示例:\n") + fmt.Fprintf(os.Stderr, " baihu resetpwd admin\n\n") +} + func Run(args []string) { - fs := flag.NewFlagSet("resetpwd", flag.ExitOnError) - fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板用户密码重置工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu resetpwd [用户名]\n\n") - fmt.Fprintf(os.Stderr, "示例:\n") - fmt.Fprintf(os.Stderr, " baihu resetpwd admin\n\n") + if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") { + printHelp() + return } + fs := flag.NewFlagSet("resetpwd", flag.ExitOnError) + fs.Usage = printHelp + if err := fs.Parse(args); err != nil { return } // 基础环境初始化 - bootstrap.InitBasic() + bootstrap.InitBasicForCmd() settingsService := services.NewSettingsService() if err := settingsService.InitSettings(); err != nil { fmt.Printf("初始化系统设置失败: %v\n", err) diff --git a/cmd/restore/restore.go b/cmd/restore/restore.go index 18391b1..f300a7f 100644 --- a/cmd/restore/restore.go +++ b/cmd/restore/restore.go @@ -10,16 +10,23 @@ import ( "github.com/engigu/baihu-panel/internal/services" ) +func printHelp() { + fmt.Fprintf(os.Stderr, "\n白虎面板系统数据恢复工具\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu restore <备份文件.zip>\n\n") + fmt.Fprintf(os.Stderr, "示例:\n") + fmt.Fprintf(os.Stderr, " baihu restore backup_20231027.zip\n\n") +} + func Run(args []string) { - fs := flag.NewFlagSet("restore", flag.ExitOnError) - fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板系统数据恢复工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu restore <备份文件.zip>\n\n") - fmt.Fprintf(os.Stderr, "示例:\n") - fmt.Fprintf(os.Stderr, " baihu restore backup_20231027.zip\n\n") + if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") { + printHelp() + return } + fs := flag.NewFlagSet("restore", flag.ExitOnError) + fs.Usage = printHelp + if err := fs.Parse(args); err != nil { return } @@ -44,7 +51,7 @@ func Run(args []string) { } // 必须初始化环境与数据库才能恢复数据 - bootstrap.InitBasic() + bootstrap.InitBasicForCmd() backupService := services.NewBackupService() fmt.Printf("正在从 '%s' 恢复系统数据,请勿强制中断...\n", absPath) diff --git a/cmd/task/task.go b/cmd/task/task.go new file mode 100644 index 0000000..b2e758d --- /dev/null +++ b/cmd/task/task.go @@ -0,0 +1,399 @@ +package task + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "regexp" + "strings" + + "github.com/engigu/baihu-panel/internal/bootstrap" + "github.com/engigu/baihu-panel/internal/constant" + "github.com/engigu/baihu-panel/internal/database" + "github.com/engigu/baihu-panel/internal/models" + "github.com/engigu/baihu-panel/internal/utils" +) + +// 打印主帮助 +func printMainHelp() { + fmt.Fprintf(os.Stderr, "\n白虎面板任务命令行管理工具 (Task CLI)\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu task <子命令> [参数]\n\n") + fmt.Fprintf(os.Stderr, "可用子命令:\n") + fmt.Fprintf(os.Stderr, " list 查询并输出任务列表\n") + fmt.Fprintf(os.Stderr, " run 手动立即触发执行指定的任务\n") + fmt.Fprintf(os.Stderr, " enable 启用指定的任务(同步加入后台调度队列)\n") + fmt.Fprintf(os.Stderr, " disable 禁用指定的任务(同步从后台调度队列摘除)\n") + fmt.Fprintf(os.Stderr, " status 查看指定任务最近一次执行的完整输出与状态\n") + fmt.Fprintf(os.Stderr, " history 查看指定任务近期的多次执行流水记录\n\n") + fmt.Fprintf(os.Stderr, "使用 'baihu task <子命令> --help' 查看具体子命令的参数说明和示例。\n\n") +} + +// Run 任务命令行入口 +func Run(args []string) { + if len(args) == 0 || args[0] == "-h" || args[0] == "--help" { + printMainHelp() + return + } + + subCommand := args[0] + subArgs := args[1:] + + switch subCommand { + case "list": + runList(subArgs) + case "run": + runExecute(subArgs) + case "enable", "disable": + runToggle(subCommand, subArgs) + case "status": + runStatus(subArgs) + case "history": + runHistory(subArgs) + default: + fmt.Fprintf(os.Stderr, "未知子命令: %s\n", subCommand) + printMainHelp() + } +} + +func runList(args []string) { + fs := flag.NewFlagSet("list", flag.ExitOnError) + namePtr := fs.String("name", "", "按任务名称或备注进行模糊筛选") + typePtr := fs.String("type", "", "按任务类型筛选 (例如: task, repo)") + pagePtr := fs.Int("page", 1, "查询页码") + sizePtr := fs.Int("size", 20, "每页展示条数") + + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "\n白虎面板任务列表查询工具\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu task list [参数]\n\n") + fmt.Fprintf(os.Stderr, "参数说明:\n") + fs.PrintDefaults() + fmt.Fprintf(os.Stderr, "\n示例:\n") + fmt.Fprintf(os.Stderr, " baihu task list\n") + fmt.Fprintf(os.Stderr, " baihu task list -page 2 -size 10\n") + fmt.Fprintf(os.Stderr, " baihu task list -name \"签到\"\n\n") + } + + if err := fs.Parse(args); err != nil { + return + } + + // 基础环境初始化 + bootstrap.InitBasicForCmd() + + var total int64 + query := database.DB.Model(&models.Task{}) + if *namePtr != "" { + query = query.Where("name LIKE ? OR remark LIKE ?", "%"+*namePtr+"%", "%"+*namePtr+"%") + } + if *typePtr != "" { + query = query.Where("type = ?", *typePtr) + } + query.Count(&total) + + offset := (*pagePtr - 1) * *sizePtr + if offset < 0 { + offset = 0 + } + + var tasks []models.Task + query.Order("created_at DESC").Limit(*sizePtr).Offset(offset).Find(&tasks) + + fmt.Println("====================================================================================================") + fmt.Printf("%-12s | %-25s | %-18s | %-8s | %-6s\n", "任务ID", "任务名称", "Cron规则", "类型", "状态") + fmt.Println("----------------------------------------------------------------------------------------------------") + for _, t := range tasks { + name := t.Name + // 截断过长名称以对齐 + if len(name) > 22 { + name = string([]rune(name)[:20]) + ".." + } + cron := t.Schedule + if cron == "" { + cron = "-" + } + status := "启用" + if !utils.DerefBool(t.Enabled, true) { + status = "禁用" + } + fmt.Printf("%-12s | %-25s | %-18s | %-8s | %-6s\n", t.ID, name, cron, t.Type, status) + } + fmt.Println("====================================================================================================") + totalPages := (total + int64(*sizePtr) - 1) / int64(*sizePtr) + if totalPages == 0 { + totalPages = 1 + } + fmt.Printf("共查询到 %d 个任务记录,当前展示第 %d/%d 页 (每页 %d 条)。\n", total, *pagePtr, totalPages, *sizePtr) + fmt.Printf("提示: 追加参数 (例如 '-page 2 -size 50') 即可灵活查看指定页码或调整展示数量。\n") +} + +func runExecute(args []string) { + fs := flag.NewFlagSet("run", flag.ExitOnError) + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "\n白虎面板手动任务触发工具\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu task run <任务ID>\n\n") + fmt.Fprintf(os.Stderr, "示例:\n") + fmt.Fprintf(os.Stderr, " baihu task run a1b2c3d4\n\n") + } + + if err := fs.Parse(args); err != nil { + return + } + + parsedArgs := fs.Args() + if len(parsedArgs) < 1 { + fmt.Fprintf(os.Stderr, "错误: 缺少目标任务ID。\n") + fs.Usage() + return + } + taskID := parsedArgs[0] + + bootstrap.InitBasicForCmd() + + bodyBytes, statusCode, err := bootstrap.SendInternalRequest("POST", "/internal/tasks/execute/"+taskID, map[string]interface{}{}) + if err != nil { + fmt.Printf(">> 触发请求失败: %v\n", err) + return + } + + if statusCode == 200 { + var res struct { + Data struct { + Success bool `json:"success"` + Error string `json:"error"` + } `json:"data"` + } + _ = json.Unmarshal(bodyBytes, &res) + if res.Data.Success { + fmt.Printf(">> 任务 [%s] 触发指令下发成功!已进入后台调度队列排队或执行。\n", taskID) + fmt.Printf(">> 提示: 可以使用 'baihu task status %s' 查看近期执行输出。\n", taskID) + } else { + errReason := res.Data.Error + if errReason == "" { + errReason = "未知调度拒绝原因 (原始返回: " + strings.TrimSpace(string(bodyBytes)) + ")" + } + fmt.Printf(">> 任务触发被拒绝,原因: %s\n", errReason) + } + } else { + fmt.Printf(">> 触发失败,后台返回状态码: %d\n响应内容: %s\n", statusCode, string(bodyBytes)) + } +} + +func runToggle(action string, args []string) { + fs := flag.NewFlagSet(action, flag.ExitOnError) + actionName := "启用" + targetEnabled := true + if action == "disable" { + actionName = "禁用" + targetEnabled = false + } + + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "\n白虎面板任务%s工具\n\n", actionName) + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu task %s <任务ID>\n\n", action) + fmt.Fprintf(os.Stderr, "示例:\n") + fmt.Fprintf(os.Stderr, " baihu task %s a1b2c3d4\n\n", action) + } + + if err := fs.Parse(args); err != nil { + return + } + + parsedArgs := fs.Args() + if len(parsedArgs) < 1 { + fmt.Fprintf(os.Stderr, "错误: 缺少目标任务ID。\n") + fs.Usage() + return + } + taskID := parsedArgs[0] + + bootstrap.InitBasicForCmd() + + bodyBytes, statusCode, err := bootstrap.SendInternalRequest("POST", "/internal/tasks/toggle/"+taskID, map[string]interface{}{ + "enabled": targetEnabled, + }) + if err != nil { + fmt.Printf(">> 切换状态请求失败: %v\n", err) + return + } + + if statusCode == 200 { + fmt.Printf(">> 任务 [%s] 已成功%s!\n", taskID, actionName) + } else { + fmt.Printf(">> 操作失败,后台返回状态码: %d\n响应内容: %s\n", statusCode, string(bodyBytes)) + } +} + +var ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]") + +func runStatus(args []string) { + fs := flag.NewFlagSet("status", flag.ExitOnError) + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "\n白虎面板任务执行状态与日志查看工具\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu task status <任务ID> [日志ID]\n\n") + fmt.Fprintf(os.Stderr, "说明:\n") + fmt.Fprintf(os.Stderr, " 默认查看该任务最新的一条日志记录。若指定了具体的日志ID,则查看特定历史日志的内容。\n\n") + fmt.Fprintf(os.Stderr, "示例:\n") + fmt.Fprintf(os.Stderr, " baihu task status a1b2c3d4\n") + fmt.Fprintf(os.Stderr, " baihu task status a1b2c3d4 log_123456\n\n") + } + + if err := fs.Parse(args); err != nil { + return + } + + parsedArgs := fs.Args() + if len(parsedArgs) < 1 { + fmt.Fprintf(os.Stderr, "错误: 缺少目标任务ID。\n") + fs.Usage() + return + } + taskID := parsedArgs[0] + var specificLogID string + if len(parsedArgs) > 1 { + specificLogID = parsedArgs[1] + } + + bootstrap.InitBasicForCmd() + + var taskLog models.TaskLog + query := database.DB.Where("task_id = ?", taskID) + if specificLogID != "" { + query = query.Where("id = ?", specificLogID) + } + res := query.Order("created_at DESC").Limit(1).Find(&taskLog) + if res.Error != nil || res.RowsAffected == 0 { + if specificLogID != "" { + fmt.Printf("找不到任务 [%s] 指定日志ID [%s] 的记录。\n", taskID, specificLogID) + } else { + fmt.Printf("找不到任务 [%s] 的任何执行记录。\n", taskID) + } + return + } + + var task models.Task + database.DB.Where("id = ?", taskID).Limit(1).Find(&task) + taskName := taskID + if task.Name != "" { + taskName = task.Name + } + + statusText := "运行中" + switch taskLog.Status { + case constant.TaskStatusSuccess: + statusText = "成功" + case constant.TaskStatusFailed: + statusText = "失败" + case constant.TaskStatusTimeout: + statusText = "超时" + case constant.TaskStatusCancelled: + statusText = "已取消" + } + + fmt.Println("====================================================================================================") + fmt.Printf("任务名称: %s (ID: %s)\n", taskName, taskID) + fmt.Printf("日志记录: %s\n", taskLog.ID) + fmt.Printf("执行命令: %s\n", string(taskLog.Command)) + fmt.Printf("最终状态: %s (耗时: %d 毫秒, 退出码: %d)\n", statusText, taskLog.Duration, taskLog.ExitCode) + if taskLog.StartTime != nil { + fmt.Printf("开始时间: %s\n", taskLog.StartTime.Time().Format("2006-01-02 15:04:05")) + } + fmt.Println("----------------------------------------------------------------------------------------------------") + fmt.Println("[日志输出内容]") + + // 解压 + decompressed, err := utils.DecompressFromBase64(string(taskLog.Output)) + if err != nil { + fmt.Printf("[无法解压日志输出: %v]\n", err) + } else { + // 清理多余回车和终端 ANSI 转义字符 + cleanText := strings.ReplaceAll(decompressed, "\r\n", "\n") + cleanText = ansiRegex.ReplaceAllString(cleanText, "") + fmt.Println(strings.TrimSpace(cleanText)) + } + + if string(taskLog.Error) != "" { + fmt.Println("\n[系统捕获异常]") + fmt.Println(string(taskLog.Error)) + } + fmt.Println("====================================================================================================") +} + +func runHistory(args []string) { + fs := flag.NewFlagSet("history", flag.ExitOnError) + limitPtr := fs.Int("limit", 10, "展示的最近历史记录条数") + + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "\n白虎面板任务执行历史查看工具\n\n") + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " baihu task history <任务ID> [参数]\n\n") + fmt.Fprintf(os.Stderr, "参数说明:\n") + fs.PrintDefaults() + fmt.Fprintf(os.Stderr, "\n示例:\n") + fmt.Fprintf(os.Stderr, " baihu task history a1b2c3d4\n") + fmt.Fprintf(os.Stderr, " baihu task history a1b2c3d4 -limit 20\n\n") + } + + if err := fs.Parse(args); err != nil { + return + } + + parsedArgs := fs.Args() + if len(parsedArgs) < 1 { + fmt.Fprintf(os.Stderr, "错误: 缺少目标任务ID。\n") + fs.Usage() + return + } + taskID := parsedArgs[0] + + bootstrap.InitBasicForCmd() + + var task models.Task + database.DB.Where("id = ?", taskID).Limit(1).Find(&task) + taskName := taskID + if task.Name != "" { + taskName = task.Name + } + + var logs []models.TaskLog + database.DB.Where("task_id = ?", taskID).Order("created_at DESC").Limit(*limitPtr).Find(&logs) + + fmt.Println("====================================================================================================") + fmt.Printf("任务流水: %s (ID: %s) 的近期执行记录 (最多展示 %d 条)\n", taskName, taskID, *limitPtr) + fmt.Println("----------------------------------------------------------------------------------------------------") + fmt.Printf("%-20s | %-8s | %-6s | %-12s | %-20s\n", "日志ID", "状态", "退出码", "耗时", "开始时间") + fmt.Println("----------------------------------------------------------------------------------------------------") + + if len(logs) == 0 { + fmt.Println("未查询到任何历史执行记录。") + } else { + for _, l := range logs { + statusText := "运行中" + switch l.Status { + case constant.TaskStatusSuccess: + statusText = "成功" + case constant.TaskStatusFailed: + statusText = "失败" + case constant.TaskStatusTimeout: + statusText = "超时" + case constant.TaskStatusCancelled: + statusText = "已取消" + } + + startStr := "-" + if l.StartTime != nil { + startStr = l.StartTime.Time().Format("2006-01-02 15:04:05") + } + durationStr := fmt.Sprintf("%d ms", l.Duration) + + fmt.Printf("%-20s | %-8s | %-6d | %-12s | %-20s\n", l.ID, statusText, l.ExitCode, durationStr, startStr) + } + } + fmt.Println("====================================================================================================") + fmt.Printf("提示: 结合命令 'baihu task status %s <日志ID>' 查看特定历史日志内容。\n", taskID) +} diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index aaedcc7..af167b2 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -42,10 +42,17 @@ func InitBasic() *App { return app } -func (a *App) initConfig() { - a.initConfigWithPath(constant.ConfigPath) +// InitBasicForCmd 专为命令行工具定制的基础环境初始化入口 +// 内部会调高控制台日志过滤级别以自动静默屏蔽刷屏的底层系统与组件启动 Info 日志 +func InitBasicForCmd() *App { + logger.SetLevel("warn") + return InitBasic() } +// func (a *App) initConfig() { +// a.initConfigWithPath(constant.ConfigPath) +// } + func (a *App) initConfigWithPath(path string) { cfg, err := services.LoadConfig(path) if err != nil { diff --git a/internal/bootstrap/client.go b/internal/bootstrap/client.go new file mode 100644 index 0000000..b5ea7b4 --- /dev/null +++ b/internal/bootstrap/client.go @@ -0,0 +1,58 @@ +package bootstrap + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/engigu/baihu-panel/internal/services" +) + +// SendInternalRequest 向常驻后台主服务安全发送内部通信请求 +// relPath 传入相对内部接口路径 (如: "/internal/tasks/execute/xxx"),方法内部会自动补充完整的协议、端口及 "/api/v1" 前缀, +// 并自动获取 security.secret 密钥种入 X-Internal-Token 头部。 +func SendInternalRequest(method, relPath string, payload interface{}) ([]byte, int, error) { + appCfg := services.GetConfig() + if appCfg == nil { + return nil, 0, fmt.Errorf("加载系统配置失败") + } + + relPath = strings.TrimPrefix(relPath, "/") + url := fmt.Sprintf("http://127.0.0.1:%d/api/v1/%s", appCfg.Server.Port, relPath) + + var bodyReader io.Reader + if payload != nil { + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, 0, fmt.Errorf("序列化请求负载失败: %v", err) + } + bodyReader = bytes.NewBuffer(jsonData) + } + + settings := services.NewSettingsService() + secret := settings.Get("security", "secret") + + req, err := http.NewRequest(method, url, bodyReader) + if err != nil { + return nil, 0, fmt.Errorf("创建 HTTP 请求失败: %v", err) + } + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + req.Header.Set("X-Internal-Token", secret) + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, 0, fmt.Errorf("网络连接失败,请确保白虎面板常驻后台服务正在运行中: %v", err) + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + return bodyBytes, resp.StatusCode, err +} diff --git a/internal/constant/commands.go b/internal/constant/commands.go index d1afc13..14cde4f 100644 --- a/internal/constant/commands.go +++ b/internal/constant/commands.go @@ -28,4 +28,8 @@ var Commands = []CommandInfo{ Name: "builtininstall", Description: "为所有 mise 管理的 Node.js 和 Python 环境安装内建助手库", }, + { + Name: "task", + Description: "系统级任务的列表查询、触发运行、启停控制及状态查看", + }, } diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index b7bfe5a..de27df9 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -619,3 +619,80 @@ func (tc *TaskController) SyncRepoTasks(c *gin.Context) { tc.executorService.SyncRepoTasks(req.UpsertedIDs, req.DeletedIDs) utils.SuccessMsg(c, "增量同步成功") } + +// ToggleTask 切换任务启用/禁用状态 +func (tc *TaskController) ToggleTask(c *gin.Context) { + id := c.Param("id") + if id == "" { + utils.BadRequest(c, "无效的任务ID") + return + } + + var req struct { + Enabled bool `json:"enabled"` + } + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + task := tc.taskService.GetTaskByID(id) + if task == nil { + utils.NotFound(c, "任务不存在") + return + } + + // 获取旧 AgentID + var oldAgentID *string + oldAgentID = task.AgentID + + // 构造更新参数,仅修改 Enabled + param := tasks.TaskParam{ + Name: task.Name, + Remark: task.Remark, + Command: string(task.Command), + PreCommand: string(task.PreCommand), + PostCommand: string(task.PostCommand), + Tags: task.Tags, + Type: task.Type, + Config: string(task.Config), + Schedule: task.Schedule, + Timeout: task.Timeout, + WorkDir: task.WorkDir, + CleanConfig: task.CleanConfig, + Envs: string(task.Envs), + Languages: task.Languages, + AgentID: task.AgentID, + TriggerType: task.TriggerType, + RetryCount: task.RetryCount, + RetryInterval: task.RetryInterval, + RandomRange: task.RandomRange, + SourceID: task.SourceID, + PinType: task.PinType, + Enabled: req.Enabled, + } + + updatedTask := tc.taskService.UpdateTask(id, ¶m) + if updatedTask == nil { + utils.NotFound(c, "任务不存在") + return + } + + // 处理调度器更新 + if updatedTask.AgentID != nil && *updatedTask.AgentID != "" { + tc.executorService.RemoveCronTask(updatedTask.ID) + tc.agentWSManager.BroadcastTasks(*updatedTask.AgentID) + } else { + if req.Enabled { + tc.executorService.AddCronTask(updatedTask) + } else { + tc.executorService.RemoveCronTask(updatedTask.ID) + } + if oldAgentID != nil && *oldAgentID != "" { + tc.agentWSManager.BroadcastTasks(*oldAgentID) + } + } + + utils.Success(c, vo.ToTaskVO(updatedTask)) +} + diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go index 18f6b4f..e0dd5cc 100644 --- a/internal/router/api_routes.go +++ b/internal/router/api_routes.go @@ -29,6 +29,8 @@ func initPublicAPIRoutes(api *gin.RouterGroup, c *Controllers) { internalAPI.Use(middleware.LocalhostOnly()) { internalAPI.POST("/tasks/sync-repo-status", c.Task.SyncRepoTasks) + internalAPI.POST("/tasks/execute/:id", c.Executor.ExecuteTask) + internalAPI.POST("/tasks/toggle/:id", c.Task.ToggleTask) } } diff --git a/main.go b/main.go index 64a7cf0..9f54d64 100644 --- a/main.go +++ b/main.go @@ -52,7 +52,7 @@ func main() { } if handler, ok := cmd.Handlers[commandName]; ok { - bootstrap.InitBasic() // 启动基础环境(配置和数据库) + bootstrap.InitBasicForCmd() // 专为命令行工具定制启动基础环境,屏蔽后台启动刷屏日志 handler(os.Args[2:]) return }