Initial commit: TaskPool React panel

- React frontend with route-level code splitting
- Backend rebranded from Baihu to TaskPool
- DB brand migration script and local compatibility
This commit is contained in:
2026-07-26 08:43:52 +08:00
commit e6956aa001
397 changed files with 73621 additions and 0 deletions
+398
View File
@@ -0,0 +1,398 @@
package task
import (
"flag"
"fmt"
"os"
"strings"
"github.com/engigu/taskpool/cmd/clibase"
"github.com/engigu/taskpool/internal/constant"
"github.com/engigu/taskpool/internal/database"
"github.com/engigu/taskpool/internal/models"
"github.com/engigu/taskpool/internal/utils"
)
// 打印主帮助
func printMainHelp() {
fmt.Fprintf(os.Stderr, "\n任务池任务命令行管理工具 (Task CLI)\n\n")
fmt.Fprintf(os.Stderr, "说明:\n")
fmt.Fprintf(os.Stderr, " 本工具原生兼容管理普通任务 (task) 与仓库同步任务 (repo)。\n")
fmt.Fprintf(os.Stderr, " 操作目标支持传入精确任务ID、任务名称模糊/精准查找,或使用快捷字面量 'repo' 一键操作主力仓库。\n\n")
fmt.Fprintf(os.Stderr, "用法:\n")
fmt.Fprintf(os.Stderr, " taskpool 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, "使用 'taskpool task <子命令> --help' 查看具体子命令的参数说明和示例。\n\n")
}
// resolveTaskID 智能解析目标任务ID:支持直接传入真实ID、任务名称,或传入 "repo" 快捷操作系统中唯一的仓库同步任务
func resolveTaskID(input string) string {
var t models.Task
// 1. 尝试按精确 ID 匹配
if res := database.DB.Where("id = ?", input).Limit(1).Find(&t); res.Error == nil && res.RowsAffected > 0 {
return t.ID
}
// 2. 如果输入字面量为 "repo",尝试匹配 type = 'repo' 的记录
if strings.ToLower(input) == "repo" {
var repos []models.Task
if res := database.DB.Where("type = ?", constant.TaskTypeRepo).Find(&repos); res.Error == nil {
if len(repos) == 1 {
fmt.Printf(">> 智能匹配到唯一的仓库任务: [%s] (ID: %s)\n", repos[0].Name, repos[0].ID)
return repos[0].ID
} else if len(repos) > 1 {
fmt.Fprintf(os.Stderr, ">> 提示: 系统中存在多个 repo 类型的仓库任务,请指定具体的仓库名称或ID进行精确操作。\n")
return input
}
}
}
// 3. 尝试按名称精准或模糊匹配
var namedTasks []models.Task
if res := database.DB.Where("name = ?", input).Find(&namedTasks); res.Error == nil && len(namedTasks) > 0 {
if len(namedTasks) == 1 {
fmt.Printf(">> 智能匹配到目标任务: [%s] (ID: %s)\n", namedTasks[0].Name, namedTasks[0].ID)
return namedTasks[0].ID
}
fmt.Fprintf(os.Stderr, ">> 提示: 存在多个同名任务 [%s],请使用精确的任务ID进行操作。\n", input)
return input
}
// 尝试名称模糊匹配 (LIKE)
if res := database.DB.Where("name LIKE ?", "%"+input+"%").Find(&namedTasks); res.Error == nil && len(namedTasks) == 1 {
fmt.Printf(">> 模糊匹配到唯一的任务: [%s] (ID: %s)\n", namedTasks[0].Name, namedTasks[0].ID)
return namedTasks[0].ID
}
// 默认原样返回
return input
}
// 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() {
clibase.PrintSubCommandUsage("任务池任务列表查询工具", "taskpool task list [参数]", " taskpool task list\n taskpool task list -page 2 -size 10\n taskpool task list -name \"签到\"", fs)
}
if err := fs.Parse(args); err != nil {
return
}
clibase.InitContext(false)
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(strings.Repeat("=", 90))
fmt.Printf("%s | %s | %s | %s | %s\n",
clibase.VisualFormat("任务ID", 20),
clibase.VisualFormat("任务名称", 28),
clibase.VisualFormat("Cron规则", 18),
clibase.VisualFormat("类型", 6),
clibase.VisualFormat("状态", 6),
)
fmt.Println(strings.Repeat("-", 90))
for _, t := range tasks {
cron := t.Schedule
if cron == "" {
cron = "-"
}
status := "启用"
if !utils.DerefBool(t.Enabled, true) {
status = "禁用"
}
fmt.Printf("%s | %s | %s | %s | %s\n",
clibase.VisualFormat(t.ID, 20),
clibase.VisualFormat(t.Name, 28),
clibase.VisualFormat(cron, 18),
clibase.VisualFormat(t.Type, 6),
clibase.VisualFormat(status, 6),
)
}
fmt.Println(strings.Repeat("=", 90))
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() {
clibase.PrintSubCommandUsage("任务池手动任务触发工具", "taskpool task run <任务ID/名称/repo>", " taskpool task run a1b2c3d4\n taskpool task run \"自动签到\"\n taskpool task run repo", nil)
}
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]
clibase.InitContext(false)
taskID = resolveTaskID(taskID)
_, err := clibase.CallInternalAPI("POST", "/internal/tasks/execute/"+taskID, map[string]interface{}{})
if err != nil {
fmt.Printf(">> 任务触发失败: %v\n", err)
return
}
fmt.Printf(">> 任务 [%s] 触发指令下发成功!已进入后台调度队列排队或执行。\n", taskID)
fmt.Printf(">> 提示: 可以使用 'taskpool task status %s' 查看近期执行输出。\n", taskID)
}
func runToggle(action string, args []string) {
fs := flag.NewFlagSet(action, flag.ExitOnError)
actionName := "启用"
targetEnabled := true
if action == "disable" {
actionName = "禁用"
targetEnabled = false
}
fs.Usage = func() {
clibase.PrintSubCommandUsage(fmt.Sprintf("任务池任务%s工具", actionName), fmt.Sprintf("taskpool task %s <任务ID/名称/repo>", action), fmt.Sprintf(" taskpool task %s a1b2c3d4\n taskpool task %s repo", action, action), nil)
}
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]
clibase.InitContext(false)
taskID = resolveTaskID(taskID)
_, err := clibase.CallInternalAPI("POST", "/internal/tasks/toggle/"+taskID, map[string]interface{}{
"enabled": targetEnabled,
})
if err != nil {
fmt.Printf(">> 切换状态操作失败: %v\n", err)
return
}
fmt.Printf(">> 任务 [%s] 已成功%s\n", taskID, actionName)
}
func runStatus(args []string) {
fs := flag.NewFlagSet("status", flag.ExitOnError)
fs.Usage = func() {
clibase.PrintSubCommandUsage("任务池任务执行状态与日志查看工具", "taskpool task status <任务ID/名称/repo> [日志ID]", " taskpool task status a1b2c3d4\n taskpool task status repo\n taskpool task status \"自动签到\"", nil)
}
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]
}
clibase.InitContext(false)
taskID = resolveTaskID(taskID)
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 = clibase.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() {
clibase.PrintSubCommandUsage("任务池任务执行历史查看工具", "taskpool task history <任务ID/名称/repo> [参数]", " taskpool task history a1b2c3d4\n taskpool task history repo\n taskpool task history repo -limit 20", fs)
}
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]
clibase.InitContext(false)
taskID = resolveTaskID(taskID)
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("提示: 结合命令 'taskpool task status %s <日志ID>' 查看特定历史日志内容。\n", taskID)
}