From acc65dce64aa40ec7eafd058a9353b97fbc1a497 Mon Sep 17 00:00:00 2001 From: engigu Date: Thu, 26 Feb 2026 10:43:49 +0800 Subject: [PATCH] feat: add startup task and fix backup import --- internal/constant/constant.go | 4 + internal/controllers/task_controller.go | 16 +-- internal/models/task.go | 1 + internal/models/vo/task_vo.go | 2 + internal/services/backup_service.go | 102 ++++++++++---------- internal/services/tasks/executor_service.go | 19 +++- internal/services/tasks/task_service.go | 18 +++- web/dist/.gitkeep | 0 web/src/api/index.ts | 1 + web/src/constants/index.ts | 6 ++ web/src/views/tasks/TaskDialog.vue | 20 ++++ web/src/views/tasks/Tasks.vue | 8 +- 12 files changed, 131 insertions(+), 66 deletions(-) delete mode 100644 web/dist/.gitkeep diff --git a/internal/constant/constant.go b/internal/constant/constant.go index d89a222..0beef47 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -78,6 +78,10 @@ const ( TaskTypeNormal = "task" TaskTypeRepo = "repo" + // 触发类型 + TriggerTypeCron = "cron" + TriggerTypeBaihuStartup = "baihu_startup" + // Agent 状态 AgentStatusOnline = "online" AgentStatusOffline = "offline" diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index 3bba17c..39b0bf9 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -56,13 +56,14 @@ func (tc *TaskController) CreateTask(c *gin.Context) { Command string `json:"command"` Type string `json:"type"` Config string `json:"config"` - Schedule string `json:"schedule" binding:"required"` + Schedule string `json:"schedule"` Timeout int `json:"timeout"` WorkDir string `json:"work_dir"` CleanConfig string `json:"clean_config"` Envs string `json:"envs"` Languages []map[string]string `json:"languages"` AgentID *uint `json:"agent_id"` + TriggerType string `json:"trigger_type"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -76,9 +77,11 @@ func (tc *TaskController) CreateTask(c *gin.Context) { return } - if err := tc.executorService.ValidateCron(req.Schedule); err != nil { - utils.BadRequest(c, "无效的cron表达式: "+err.Error()) - return + if req.Schedule != "" { + if err := tc.executorService.ValidateCron(req.Schedule); err != nil { + utils.BadRequest(c, "无效的cron表达式: "+err.Error()) + return + } } // 转换为绝对路径(Agent 任务保持原样) @@ -87,7 +90,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) { workDir = resolveWorkDir(req.WorkDir) } - task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages) + task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType) // 如果是 Agent 任务,通知 Agent;否则添加到本地 cron if task.AgentID != nil && *task.AgentID > 0 { @@ -159,6 +162,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { Enabled bool `json:"enabled"` Languages []map[string]string `json:"languages"` AgentID *uint `json:"agent_id"` + TriggerType string `json:"trigger_type"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -179,7 +183,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { workDir = resolveWorkDir(req.WorkDir) } - task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages) + task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType) if task == nil { utils.NotFound(c, "任务不存在") return diff --git a/internal/models/task.go b/internal/models/task.go index 535b7b9..d244a55 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -38,6 +38,7 @@ type Task struct { Name string `json:"name" gorm:"size:255;not null"` Command string `json:"command" gorm:"type:text"` // 普通任务的命令 Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: constant.TaskTypeNormal, constant.TaskTypeRepo + TriggerType string `json:"trigger_type" gorm:"size:25;default:'cron'"` // 触发类型: constant.TriggerTypeCron, constant.TriggerTypeBaihuStartup Config string `json:"config" gorm:"type:text"` // 配置 JSON(仓库同步配置等) Schedule string `json:"schedule" gorm:"size:100"` // cron 表达式 Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟 diff --git a/internal/models/vo/task_vo.go b/internal/models/vo/task_vo.go index 58253c7..4ef3198 100644 --- a/internal/models/vo/task_vo.go +++ b/internal/models/vo/task_vo.go @@ -11,6 +11,7 @@ type TaskVO struct { Name string `json:"name"` Command string `json:"command"` Type string `json:"type"` + TriggerType string `json:"trigger_type"` Config string `json:"config"` Schedule string `json:"schedule"` Timeout int `json:"timeout"` @@ -36,6 +37,7 @@ func ToTaskVO(task *models.Task) *TaskVO { Name: task.Name, Command: task.Command, Type: task.Type, + TriggerType: task.TriggerType, Config: task.Config, Schedule: task.Schedule, Timeout: task.Timeout, diff --git a/internal/services/backup_service.go b/internal/services/backup_service.go index 28a282a..d1d1812 100644 --- a/internal/services/backup_service.go +++ b/internal/services/backup_service.go @@ -227,6 +227,32 @@ func (s *BackupService) Restore(zipPath string) error { }) } +func restoreStreamBatch[T any](tx *gorm.DB, decoder *json.Decoder) error { + batchSize := 1000 + var batch []*T + + for decoder.More() { + var m T + if err := decoder.Decode(&m); err != nil { + return err + } + batch = append(batch, &m) + + if len(batch) >= batchSize { + if err := tx.CreateInBatches(batch, batchSize).Error; err != nil { + return err + } + batch = nil // reset batch + } + } + + if len(batch) > 0 { + return tx.CreateInBatches(batch, len(batch)).Error + } + + return nil +} + func (s *BackupService) restoreFromZipFile(tx *gorm.DB, f *zip.File, filename string) error { rc, err := f.Open() if err != nil { @@ -254,60 +280,30 @@ func (s *BackupService) restoreFromZipFile(tx *gorm.DB, f *zip.File, filename st return fmt.Errorf("invalid json format: expected %s", filename) } - batchSize := 1000 - var batch []any - - // 根据文件名确定模型类型 - getModel := func() any { - switch filename { - case "tasks.json": - return &models.Task{} - case "task_logs.json": - return &models.TaskLog{} - case "envs.json": - return &models.EnvironmentVariable{} - case "scripts.json": - return &models.Script{} - case "send_stats.json": - return &models.SendStats{} - case "login_logs.json": - return &models.LoginLog{} - case "agents.json": - return &models.Agent{} - case "tokens.json": - return &models.AgentToken{} - case "languages.json": - return &models.Language{} - case "deps.json": - return &models.Dependency{} - default: - return nil - } + switch filename { + case "tasks.json": + return restoreStreamBatch[models.Task](tx, decoder) + case "task_logs.json": + return restoreStreamBatch[models.TaskLog](tx, decoder) + case "envs.json": + return restoreStreamBatch[models.EnvironmentVariable](tx, decoder) + case "scripts.json": + return restoreStreamBatch[models.Script](tx, decoder) + case "send_stats.json": + return restoreStreamBatch[models.SendStats](tx, decoder) + case "login_logs.json": + return restoreStreamBatch[models.LoginLog](tx, decoder) + case "agents.json": + return restoreStreamBatch[models.Agent](tx, decoder) + case "tokens.json": + return restoreStreamBatch[models.AgentToken](tx, decoder) + case "languages.json": + return restoreStreamBatch[models.Language](tx, decoder) + case "deps.json": + return restoreStreamBatch[models.Dependency](tx, decoder) + default: + return nil } - - for decoder.More() { - m := getModel() - if m == nil { - break - } - if err := decoder.Decode(m); err != nil { - return err - } - batch = append(batch, m) - - if len(batch) >= batchSize { - if err := tx.CreateInBatches(batch, batchSize).Error; err != nil { - return err - } - batch = nil - } - } - - if len(batch) > 0 { - return tx.CreateInBatches(batch, batchSize).Error - } - - return nil } // insertRecords, restoreFromData 方法已合并入 restoreFromZipFile,此处删除冗余方法 diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index a6f9f21..cf5d95d 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -416,6 +416,10 @@ func (es *ExecutorService) StopCron() { // AddCronTask 添加计划任务 func (es *ExecutorService) AddCronTask(task *models.Task) error { + if task.TriggerType != constant.TriggerTypeCron { + es.RemoveCronTask(task.ID) // 如果不是cron类型,确保从调度器移除 + return nil + } return es.cronManager.AddTask(task) } @@ -439,8 +443,19 @@ func (es *ExecutorService) loadCronTasks() { tasks := es.taskService.GetTasks() count := 0 for _, task := range tasks { - // 只调度本地任务(agent_id 为空或 0) - if task.Enabled && (task.AgentID == nil || *task.AgentID == 0) { + if !task.Enabled { + continue + } + + if task.TriggerType == constant.TriggerTypeBaihuStartup { + go func(t models.Task) { + // 延迟一点时间再触发,确保系统完全启动 + time.Sleep(3 * time.Second) + logger.Infof("[Executor] 触发开机服务启动任务 #%d: %s", t.ID, t.Name) + es.ExecuteTask(int(t.ID), nil) + }(task) + } else if task.TriggerType == constant.TriggerTypeCron && task.Schedule != "" && (task.AgentID == nil || *task.AgentID == 0) { + // 只调度本地任务(agent_id 为空或 0)的定时任务 err := es.cronManager.AddTask(&task) if err != nil { continue diff --git a/internal/services/tasks/task_service.go b/internal/services/tasks/task_service.go index 8744d4e..2765c61 100644 --- a/internal/services/tasks/task_service.go +++ b/internal/services/tasks/task_service.go @@ -1,6 +1,7 @@ package tasks import ( + "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/models" ) @@ -11,14 +12,18 @@ func NewTaskService() *TaskService { return &TaskService{} } -func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, languages []map[string]string) *models.Task { +func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, languages []map[string]string, triggerType string) *models.Task { if taskType == "" { taskType = "task" } + if triggerType == "" { + triggerType = constant.TriggerTypeCron + } task := &models.Task{ Name: name, Command: command, Type: taskType, + TriggerType: triggerType, Config: config, Schedule: schedule, Timeout: timeout, @@ -29,6 +34,9 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w AgentID: agentID, Enabled: true, } + if triggerType != constant.TriggerTypeCron { + task.NextRun = nil + } database.DB.Create(task) return task } @@ -66,7 +74,7 @@ func (ts *TaskService) GetTaskByID(id int) *models.Task { return &task } -func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, languages []map[string]string) *models.Task { +func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, languages []map[string]string, triggerType string) *models.Task { var task models.Task if err := database.DB.First(&task, id).Error; err != nil { return nil @@ -84,6 +92,12 @@ func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeou if taskType != "" { task.Type = taskType } + if triggerType != "" { + task.TriggerType = triggerType + } + if task.TriggerType != constant.TriggerTypeCron { + task.NextRun = nil + } task.Config = config database.DB.Save(&task) return &task diff --git a/web/dist/.gitkeep b/web/dist/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 6e3faae..feb9ead 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -266,6 +266,7 @@ export interface Task { name: string command: string type: string + trigger_type: string config: string schedule: string timeout: number diff --git a/web/src/constants/index.ts b/web/src/constants/index.ts index 3909254..5c78577 100644 --- a/web/src/constants/index.ts +++ b/web/src/constants/index.ts @@ -34,6 +34,12 @@ export const TASK_TYPE = { REPO: 'repo', } as const +// 触发类型 +export const TRIGGER_TYPE = { + CRON: 'cron', + BAIHU_STARTUP: 'baihu_startup', +} as const + // Agent 状态 export const AGENT_STATUS = { ONLINE: 'online', diff --git a/web/src/views/tasks/TaskDialog.vue b/web/src/views/tasks/TaskDialog.vue index c2b35c4..11e6e42 100644 --- a/web/src/views/tasks/TaskDialog.vue +++ b/web/src/views/tasks/TaskDialog.vue @@ -12,6 +12,7 @@ import DirTreeSelect from '@/components/DirTreeSelect.vue' import { Plus, ChevronDown, X, Search, Check, ChevronsUpDown, Loader2, AlertCircle } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { api, type Task, type EnvVar, type Agent, type MiseLanguage } from '@/api' +import { TRIGGER_TYPE } from '@/constants' import { toast } from 'vue-sonner' const props = defineProps<{ @@ -44,6 +45,7 @@ const allEnvVars = ref([]) const allAgents = ref([]) const selectedEnvIds = ref([]) const selectedAgentId = ref('local') +const selectedTriggerType = ref('cron') const envSearchQuery = ref('') // 为每个执行位置保存独立的工作目录配置 const workDirCache = ref>({}) @@ -254,6 +256,8 @@ watch(() => props.open, async (val) => { // 解析 Agent 和工作目录 const agentId = props.task?.agent_id ? String(props.task.agent_id) : 'local' selectedAgentId.value = agentId + // 解析触发类型 + selectedTriggerType.value = props.task?.trigger_type || TRIGGER_TYPE.CRON // 初始化工作目录缓存,将当前任务的工作目录保存到对应的执行位置 workDirCache.value = { [agentId]: props.task?.work_dir || '' @@ -298,6 +302,7 @@ async function save() { form.value.clean_config = cleanConfig.value form.value.envs = selectedEnvIds.value.join(',') form.value.type = 'task' + form.value.trigger_type = selectedTriggerType.value form.value.agent_id = selectedAgentId.value === 'local' ? null : Number(selectedAgentId.value) // 保存语言环境配置 @@ -374,6 +379,21 @@ async function save() { +
+ +
+ +
+
+