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)
}
+9 -2
View File
@@ -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 {
+58
View File
@@ -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
}
+4
View File
@@ -28,4 +28,8 @@ var Commands = []CommandInfo{
Name: "builtininstall",
Description: "为所有 mise 管理的 Node.js 和 Python 环境安装内建助手库",
},
{
Name: "task",
Description: "系统级任务的列表查询、触发运行、启停控制及状态查看",
},
}
+77
View File
@@ -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, &param)
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))
}
+2
View File
@@ -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)
}
}
+1 -1
View File
@@ -52,7 +52,7 @@ func main() {
}
if handler, ok := cmd.Handlers[commandName]; ok {
bootstrap.InitBasic() // 启动基础环境(配置和数据库)
bootstrap.InitBasicForCmd() // 专为命令行工具定制启动基础环境,屏蔽后台启动刷屏日志
handler(os.Args[2:])
return
}