feat: add startup task and fix backup import
This commit is contained in:
@@ -78,6 +78,10 @@ const (
|
||||
TaskTypeNormal = "task"
|
||||
TaskTypeRepo = "repo"
|
||||
|
||||
// 触发类型
|
||||
TriggerTypeCron = "cron"
|
||||
TriggerTypeBaihuStartup = "baihu_startup"
|
||||
|
||||
// Agent 状态
|
||||
AgentStatusOnline = "online"
|
||||
AgentStatusOffline = "offline"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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分钟
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,此处删除冗余方法
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
@@ -266,6 +266,7 @@ export interface Task {
|
||||
name: string
|
||||
command: string
|
||||
type: string
|
||||
trigger_type: string
|
||||
config: string
|
||||
schedule: string
|
||||
timeout: number
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<EnvVar[]>([])
|
||||
const allAgents = ref<Agent[]>([])
|
||||
const selectedEnvIds = ref<number[]>([])
|
||||
const selectedAgentId = ref<string>('local')
|
||||
const selectedTriggerType = ref<string>('cron')
|
||||
const envSearchQuery = ref('')
|
||||
// 为每个执行位置保存独立的工作目录配置
|
||||
const workDirCache = ref<Record<string, string>>({})
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-3">
|
||||
<Label class="sm:text-right text-sm">触发类型</Label>
|
||||
<div class="sm:col-span-3">
|
||||
<Select v-model="selectedTriggerType">
|
||||
<SelectTrigger class="h-8 text-sm">
|
||||
<SelectValue placeholder="定时触发" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="TRIGGER_TYPE.CRON">定时触发</SelectItem>
|
||||
<SelectItem :value="TRIGGER_TYPE.BAIHU_STARTUP">服务启动时触发</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地任务语言版本配置 -->
|
||||
<template v-if="selectedAgentId === 'local'">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-start gap-2 sm:gap-3">
|
||||
|
||||
@@ -11,7 +11,7 @@ import { api, type Task, type Agent } from '@/api'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { TASK_TYPE, AGENT_STATUS } from '@/constants'
|
||||
import { TASK_TYPE, AGENT_STATUS, TRIGGER_TYPE } from '@/constants'
|
||||
import TextOverflow from '@/components/TextOverflow.vue'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -261,8 +261,10 @@ watch(() => route.query.agent_id, (newVal) => {
|
||||
class="w-32 sm:flex-1 shrink-0 sm:shrink text-muted-foreground truncate text-xs bg-muted/40 px-2 py-1 rounded hidden sm:block">
|
||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" />
|
||||
</code>
|
||||
<code class="w-36 shrink-0 text-muted-foreground text-xs bg-muted/40 px-2 py-1 rounded hidden md:block">{{ task.schedule
|
||||
}}</code>
|
||||
<div class="w-36 shrink-0 hidden md:flex flex-col items-start justify-center gap-1 overflow-hidden">
|
||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP" class="text-[10px] leading-none bg-primary/10 text-primary px-1.5 py-1 rounded whitespace-nowrap border border-primary/20">服务启动时</span>
|
||||
<code v-else-if="task.schedule" class="text-muted-foreground text-xs bg-muted/40 px-1.5 py-0.5 rounded truncate max-w-full" :title="task.schedule">{{ task.schedule }}</code>
|
||||
</div>
|
||||
<span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.last_run || '-' }}</span>
|
||||
<span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.next_run || '-' }}</span>
|
||||
<span class="w-8 sm:w-12 flex justify-center shrink-0 cursor-pointer group"
|
||||
|
||||
Reference in New Issue
Block a user