feat: add cmd task call

This commit is contained in:
duorameng
2026-05-13 17:17:50 +08:00
parent da2b003b83
commit f498c4fcd5
13 changed files with 619 additions and 58 deletions
+14 -7
View File
@@ -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
}
+2
View File
@@ -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,
}
+1 -1
View File
@@ -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
+22 -31
View File
@@ -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)
}
}
+15 -8
View File
@@ -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)
+15 -8
View File
@@ -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)
+399
View File
@@ -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)
}